v0.1.0 d9218d6

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}
45
46func fourcc(a, b, c, d byte) uint32 {
47	return uint32(a) | uint32(b)<<8 | uint32(c)<<16 | uint32(d)<<24
48}
49
50func vkFormat(f uint32) (vk.Format, bool, bool) {
51	for _, v := range dmabufFormats {
52		if v.fourcc == f {
53			return v.format, v.opaque, true
54		}
55	}
56	return 0, false, false
57}
58
59// hasExtensions reports whether the device offers every dmabuf extension.
60func (r *Renderer) hasExtensions(physical vk.PhysicalDevice) bool {
61	var n uint32
62	if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, nil) != vk.Success || n == 0 {
63		return false
64	}
65	props := make([]vk.ExtensionProperties, n)
66	if r.id.EnumerateDeviceExtensionProperties(physical, nil, &n, &props[0]) != vk.Success {
67		return false
68	}
69	have := map[string]bool{}
70	for _, p := range props[:n] {
71		have[cstring(p.ExtensionName[:])] = true
72	}
73	for _, e := range deviceExtensions {
74		if !have[e] {
75			return false
76		}
77	}
78	return true
79}
80
81func cstring(b []byte) string {
82	if i := slices.Index(b, 0); i >= 0 {
83		b = b[:i]
84	}
85	return string(b)
86}
87
88// probeDMABuf lists the fourcc/modifier pairs the device imports as
89// single-plane sampled images with linear filtering, and the render node.
90func (r *Renderer) probeDMABuf(physical vk.PhysicalDevice) ports.DMABufSupport {
91	var sup ports.DMABufSupport
92	drm := vk.PhysicalDeviceDrmPropertiesEXT{SType: vk.StructureTypePhysicalDeviceDRMPropertiesEXT}
93	props := vk.PhysicalDeviceProperties2{SType: vk.StructureTypePhysicalDeviceProperties2, Next: unsafe.Pointer(&drm)}
94	r.id.GetPhysicalDeviceProperties2(physical, &props)
95	if drm.HasRender != 0 {
96		sup.Device = unix.Mkdev(uint32(drm.RenderMajor), uint32(drm.RenderMinor))
97	}
98	for _, f := range dmabufFormats {
99		list := vk.DrmFormatModifierPropertiesListEXT{SType: vk.StructureTypeDRMFormatModifierPropertiesListEXT}
100		fp := vk.FormatProperties2{SType: vk.StructureTypeFormatProperties2, Next: unsafe.Pointer(&list)}
101		r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)
102		if list.DrmFormatModifierCount == 0 {
103			continue
104		}
105		mods := make([]vk.DrmFormatModifierPropertiesEXT, list.DrmFormatModifierCount)
106		list.DrmFormatModifierProperties = &mods[0]
107		r.id.GetPhysicalDeviceFormatProperties2(physical, f.format, &fp)
108		for _, m := range mods[:list.DrmFormatModifierCount] {
109			// Multi-plane modifiers (compression metadata) are left to a
110			// later step.
111			need := vk.FormatFeatureFlags(vk.FormatFeatureSampledImageBit | vk.FormatFeatureSampledImageFilterLinearBit)
112			if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&need != need {
113				continue
114			}
115			if r.importable(physical, f.format, m.DrmFormatModifier) {
116				sup.Formats = append(sup.Formats, ports.DMABufFormat{Format: f.fourcc, Modifier: m.DrmFormatModifier})
117			}
118		}
119	}
120	return sup
121}
122
123// importable asks whether an image of that format and modifier can be
124// imported from a dmabuf.
125func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
126	mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
127	ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
128	info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageSampledBit}
129	extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
130	out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
131	if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
132		return false
133	}
134	return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureImportableBit != 0
135}
136
137// DMABuf reports the formats this renderer imports.
138func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
139
140// imported is a client dmabuf bound to a VkImage, with the view and set
141// the compose shader samples it through.
142type imported struct {
143	image  vk.Image
144	memory vk.DeviceMemory
145	view   vk.ImageView
146	pool   vk.DescriptorPool
147	set    vk.DescriptorSet
148	fd     int // our duplicate of plane 0, for implicit sync
149	// last is the frame that drew it, for eviction.
150	last uint64
151}
152
153// importTTL is how many frames an import survives undrawn: clients cycle
154// through 2-4 buffers, and a destroyed buffer is freed shortly after.
155const importTTL = 120
156
157// importDMABuf returns the image of a client buffer, importing it once.
158func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
159	if im := r.imports[b.ID]; im != nil {
160		im.last = r.frame
161		return im, nil
162	}
163	if len(r.dmabuf.Formats) == 0 {
164		return nil, fmt.Errorf("dmabuf unsupported")
165	}
166	format, _, ok := vkFormat(b.Format)
167	if !ok || len(b.Planes) != 1 || b.Width <= 0 || b.Height <= 0 {
168		return nil, fmt.Errorf("dmabuf format %#x with %d planes unsupported", b.Format, len(b.Planes))
169	}
170	fd, err := dupFile(b.Planes[0].File)
171	if err != nil {
172		return nil, err
173	}
174	im := &imported{fd: fd, last: r.frame}
175	ok = false
176	defer func() {
177		if !ok {
178			r.release(im)
179		}
180	}()
181	d := r.dd
182	layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}
183	explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}
184	external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
185	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}
186	if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {
187		return nil, err
188	}
189	fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
190	if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
191		return nil, err
192	}
193	var req vk.MemoryRequirements
194	d.GetImageMemoryRequirements(r.device, im.image, &req)
195	kind, err := r.findMemoryType(req.MemoryTypeBits&fdProps.MemoryTypeBits, 0)
196	if err != nil {
197		return nil, err
198	}
199	// Vulkan owns the fd it imports on success: hand it its own duplicate.
200	memFD, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
201	if err != nil {
202		return nil, err
203	}
204	dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: im.image}
205	imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(memFD)}
206	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
207	if err := checked("vkAllocateMemory(dmabuf)", d.AllocateMemory(r.device, &alloc, nil, &im.memory)); err != nil {
208		unix.Close(memFD)
209		return nil, err
210	}
211	if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {
212		return nil, err
213	}
214	if im.view, err = r.imageView(im.image, format); err != nil {
215		return nil, err
216	}
217	if im.pool, im.set, err = r.newSet(im.view, r.compose.dummyBuffer.buffer); err != nil {
218		return nil, err
219	}
220	ok = true
221	r.imports[b.ID] = im
222	return im, nil
223}
224
225// dupFile duplicates the descriptor while the file is held open, so a close
226// on the wayland goroutine cannot swap it for a reused number.
227func dupFile(f *os.File) (int, error) {
228	if f == nil {
229		return -1, fmt.Errorf("dmabuf plane without file")
230	}
231	raw, err := f.SyscallConn()
232	if err != nil {
233		return -1, err
234	}
235	fd, dupErr := -1, error(nil)
236	if err := raw.Control(func(v uintptr) { fd, dupErr = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }); err != nil {
237		return -1, err
238	}
239	return fd, dupErr
240}
241
242func (r *Renderer) release(im *imported) {
243	if im.pool != 0 {
244		r.dd.DestroyDescriptorPool(r.device, im.pool, nil)
245	}
246	if im.view != 0 {
247		r.dd.DestroyImageView(r.device, im.view, nil)
248	}
249	if im.image != 0 {
250		r.dd.DestroyImage(r.device, im.image, nil)
251	}
252	if im.memory != 0 {
253		r.dd.FreeMemory(r.device, im.memory, nil)
254	}
255	if im.fd >= 0 {
256		unix.Close(im.fd)
257	}
258}
259
260// dropUnused frees imports not drawn for importTTL frames (the client
261// moved on to other buffers or went away), once the last frame that drew
262// them completed.
263func (r *Renderer) dropUnused() {
264	for id, im := range r.imports {
265		if r.frame-im.last > importTTL {
266			r.retire(im.last, func() { r.release(im) })
267			delete(r.imports, id)
268		}
269	}
270}
271
272// dmaBufSync is struct dma_buf_export_sync_file.
273type dmaBufSync struct {
274	flags uint32
275	fd    int32
276}
277
278// DMA_BUF_IOCTL_EXPORT_SYNC_FILE = _IOWR('b', 2, struct dma_buf_export_sync_file)
279const ioctlExportSyncFile = 0xc0086202
280const dmaBufSyncRead = 1
281
282// readFence returns a semaphore signalled when the client's pending writes
283// to the buffer finish, or 0 when the kernel cannot export them (older than
284// 5.20): the buffer is then read as is.
285func (r *Renderer) readFence(im *imported) vk.Semaphore {
286	arg := dmaBufSync{flags: dmaBufSyncRead, fd: -1}
287	if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {
288		return 0
289	}
290	return r.importSyncFD(int(arg.fd))
291}
292
293// importFence makes a wait semaphore of a sync file. The file stays the
294// caller's: Vulkan takes a duplicate, made while the file is held open.
295func (r *Renderer) importFence(f *os.File) vk.Semaphore {
296	fd, err := dupFile(f)
297	if err != nil {
298		return 0
299	}
300	return r.importSyncFD(fd)
301}
302
303// importSyncFD imports a sync file fd as a temporary semaphore payload;
304// the fd is Vulkan's on success and closed on failure.
305func (r *Renderer) importSyncFD(fd int) vk.Semaphore {
306	var sem vk.Semaphore
307	si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
308	if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {
309		unix.Close(fd)
310		return 0
311	}
312	imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: int32(fd)}
313	if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {
314		unix.Close(fd)
315		r.dd.DestroySemaphore(r.device, sem, nil)
316		return 0
317	}
318	return sem
319}
320
321// Probe reports the dmabuf formats of the device renderers use, or none
322// when Vulkan or the extensions are missing.
323func Probe() ports.DMABufSupport {
324	r, err := New(1, 1)
325	if err != nil {
326		return ports.DMABufSupport{}
327	}
328	defer r.Close()
329	return r.DMABuf()
330}

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 23

Tags 10

↑↓ moveenter openesc close