detached 0859b0f

feat(dmabuf): linux-dmabuf v4 and Vulkan import

Clients send GPU buffers through zwp_linux_dmabuf_v1 v4 with default
feedback. The Vulkan renderer imports single-plane modifiers, waits on
implicit fences via DMA_BUF_IOCTL_EXPORT_SYNC_FILE and blits them into
the output image. Imports are cached per buffer and evicted after 120
undrawn frames; params are bounds-checked against the plane file.
commit
0859b0ffcc0d7953e8225e6997e27e25162b7372
author
brice · · 2026-09-25 06:32 UTC
parent
07694a3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +1082 -24
M internal/adapters/drm/output.go +1 -1
@@ -178,7 +178,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
178178 }
179179 scene, haveScene, dirty = s, true, true
180180 case c := <-contents:
181- if c.Pixels == nil {
181+ if c.Empty() {
182182 delete(surfaces, c.ID)
183183 } else {
184184 surfaces[c.ID] = c
@@ -178,7 +178,7 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
178 }178 }
179 scene, haveScene, dirty = s, true, true179 scene, haveScene, dirty = s, true, true
180 case c := <-contents:180 case c := <-contents:
181- if c.Pixels == nil {181+ if c.Empty() {
182 delete(surfaces, c.ID)182 delete(surfaces, c.ID)
183 } else {183 } else {
184 surfaces[c.ID] = c184 surfaces[c.ID] = c
M internal/adapters/headless/output.go +1 -1
@@ -36,7 +36,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
3636 frame := 0
3737 cursorScale := -1.0 // not loaded yet
3838 update := func(c ports.SurfaceContent) {
39- if c.Pixels == nil {
39+ if c.Empty() {
4040 delete(surfaces, c.ID)
4141 } else {
4242 surfaces[c.ID] = c
@@ -36,7 +36,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
36 frame := 036 frame := 0
37 cursorScale := -1.0 // not loaded yet37 cursorScale := -1.0 // not loaded yet
38 update := func(c ports.SurfaceContent) {38 update := func(c ports.SurfaceContent) {
39- if c.Pixels == nil {39+ if c.Empty() {
40 delete(surfaces, c.ID)40 delete(surfaces, c.ID)
41 } else {41 } else {
42 surfaces[c.ID] = c42 surfaces[c.ID] = c
A internal/adapters/vulkan/dmabuf.go +297 -0
@@ -0,0 +1,297 @@
1+package vulkan
2+
3+import (
4+ "fmt"
5+ "os"
6+ "slices"
7+ "unsafe"
8+
9+ "github.com/bnema/nefertty/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 blitted into the output image. 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.
23+var 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.
35+var 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+
46+func fourcc(a, b, c, d byte) uint32 {
47+ return uint32(a) | uint32(b)<<8 | uint32(c)<<16 | uint32(d)<<24
48+}
49+
50+func 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.
60+func (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+
81+func 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 blit sources, and the render node.
90+func (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; the blit needs a source.
111+ if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vk.FormatFeatureBlitSrcBit == 0 {
112+ continue
113+ }
114+ if r.importable(physical, f.format, m.DrmFormatModifier) {
115+ sup.Formats = append(sup.Formats, ports.DMABufFormat{Format: f.fourcc, Modifier: m.DrmFormatModifier})
116+ }
117+ }
118+ }
119+ return sup
120+}
121+
122+// importable asks whether an image of that format and modifier can be
123+// imported from a dmabuf.
124+func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
125+ mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
126+ ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
127+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit}
128+ extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
129+ out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
130+ if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
131+ return false
132+ }
133+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureImportableBit != 0
134+}
135+
136+// DMABuf reports the formats this renderer imports.
137+func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
138+
139+// imported is a client dmabuf bound to a VkImage.
140+type imported struct {
141+ image vk.Image
142+ memory vk.DeviceMemory
143+ fd int // our duplicate of plane 0, for implicit sync
144+ // last is the frame that drew it, for eviction.
145+ last uint64
146+}
147+
148+// importTTL is how many frames an import survives undrawn: clients cycle
149+// through 2-4 buffers, and a destroyed buffer is freed shortly after.
150+const importTTL = 120
151+
152+// importDMABuf returns the image of a client buffer, importing it once.
153+func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
154+ if im := r.imports[b.ID]; im != nil {
155+ im.last = r.frame
156+ return im, nil
157+ }
158+ if len(r.dmabuf.Formats) == 0 {
159+ return nil, fmt.Errorf("dmabuf unsupported")
160+ }
161+ format, _, ok := vkFormat(b.Format)
162+ if !ok || len(b.Planes) != 1 || b.Width <= 0 || b.Height <= 0 {
163+ return nil, fmt.Errorf("dmabuf format %#x with %d planes unsupported", b.Format, len(b.Planes))
164+ }
165+ fd, err := dupFile(b.Planes[0].File)
166+ if err != nil {
167+ return nil, err
168+ }
169+ im := &imported{fd: fd, last: r.frame}
170+ ok = false
171+ defer func() {
172+ if !ok {
173+ r.release(im)
174+ }
175+ }()
176+ d := r.dd
177+ layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}
178+ explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}
179+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
180+ 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.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
181+ if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {
182+ return nil, err
183+ }
184+ fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
185+ if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
186+ return nil, err
187+ }
188+ var req vk.MemoryRequirements
189+ d.GetImageMemoryRequirements(r.device, im.image, &req)
190+ kind, err := r.findMemoryType(req.MemoryTypeBits&fdProps.MemoryTypeBits, 0)
191+ if err != nil {
192+ return nil, err
193+ }
194+ // Vulkan owns the fd it imports on success: hand it its own duplicate.
195+ memFD, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
196+ if err != nil {
197+ return nil, err
198+ }
199+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: im.image}
200+ imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(memFD)}
201+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
202+ if err := checked("vkAllocateMemory(dmabuf)", d.AllocateMemory(r.device, &alloc, nil, &im.memory)); err != nil {
203+ unix.Close(memFD)
204+ return nil, err
205+ }
206+ if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {
207+ return nil, err
208+ }
209+ ok = true
210+ r.imports[b.ID] = im
211+ return im, nil
212+}
213+
214+// dupFile duplicates the descriptor while the file is held open, so a close
215+// on the wayland goroutine cannot swap it for a reused number.
216+func dupFile(f *os.File) (int, error) {
217+ if f == nil {
218+ return -1, fmt.Errorf("dmabuf plane without file")
219+ }
220+ raw, err := f.SyscallConn()
221+ if err != nil {
222+ return -1, err
223+ }
224+ fd, dupErr := -1, error(nil)
225+ if err := raw.Control(func(v uintptr) { fd, dupErr = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }); err != nil {
226+ return -1, err
227+ }
228+ return fd, dupErr
229+}
230+
231+func (r *Renderer) release(im *imported) {
232+ if im.image != 0 {
233+ r.dd.DestroyImage(r.device, im.image, nil)
234+ }
235+ if im.memory != 0 {
236+ r.dd.FreeMemory(r.device, im.memory, nil)
237+ }
238+ if im.fd >= 0 {
239+ unix.Close(im.fd)
240+ }
241+}
242+
243+// dropUnused frees imports not drawn for importTTL frames: the client moved
244+// on to other buffers or went away. Called after a frame completes.
245+func (r *Renderer) dropUnused() {
246+ r.frame++
247+ for id, im := range r.imports {
248+ if r.frame-im.last > importTTL {
249+ r.release(im)
250+ delete(r.imports, id)
251+ }
252+ }
253+}
254+
255+// dmaBufSync is struct dma_buf_export_sync_file.
256+type dmaBufSync struct {
257+ flags uint32
258+ fd int32
259+}
260+
261+// DMA_BUF_IOCTL_EXPORT_SYNC_FILE = _IOWR('b', 2, struct dma_buf_export_sync_file)
262+const ioctlExportSyncFile = 0xc0086202
263+const dmaBufSyncRead = 1
264+
265+// readFence returns a semaphore signalled when the client's pending writes
266+// to the buffer finish, or 0 when the kernel cannot export them (older than
267+// 5.20): the buffer is then read as is.
268+func (r *Renderer) readFence(im *imported) vk.Semaphore {
269+ arg := dmaBufSync{flags: dmaBufSyncRead, fd: -1}
270+ if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {
271+ return 0
272+ }
273+ var sem vk.Semaphore
274+ si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
275+ if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {
276+ unix.Close(int(arg.fd))
277+ return 0
278+ }
279+ imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: arg.fd}
280+ if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {
281+ unix.Close(int(arg.fd))
282+ r.dd.DestroySemaphore(r.device, sem, nil)
283+ return 0
284+ }
285+ return sem
286+}
287+
288+// Probe reports the dmabuf formats of the device renderers use, or none
289+// when Vulkan or the extensions are missing.
290+func Probe() ports.DMABufSupport {
291+ r, err := New(1, 1)
292+ if err != nil {
293+ return ports.DMABufSupport{}
294+ }
295+ defer r.Close()
296+ return r.DMABuf()
297+}
@@ -0,0 +1,297 @@
1+package vulkan
2+
3+import (
4+ "fmt"
5+ "os"
6+ "slices"
7+ "unsafe"
8+
9+ "github.com/bnema/nefertty/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 blitted into the output image. 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.
23+var 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.
35+var 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+
46+func fourcc(a, b, c, d byte) uint32 {
47+ return uint32(a) | uint32(b)<<8 | uint32(c)<<16 | uint32(d)<<24
48+}
49+
50+func 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.
60+func (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+
81+func 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 blit sources, and the render node.
90+func (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; the blit needs a source.
111+ if m.DrmFormatModifierPlaneCount != 1 || m.DrmFormatModifierTilingFeatures&vk.FormatFeatureBlitSrcBit == 0 {
112+ continue
113+ }
114+ if r.importable(physical, f.format, m.DrmFormatModifier) {
115+ sup.Formats = append(sup.Formats, ports.DMABufFormat{Format: f.fourcc, Modifier: m.DrmFormatModifier})
116+ }
117+ }
118+ }
119+ return sup
120+}
121+
122+// importable asks whether an image of that format and modifier can be
123+// imported from a dmabuf.
124+func (r *Renderer) importable(physical vk.PhysicalDevice, format vk.Format, modifier uint64) bool {
125+ mod := vk.PhysicalDeviceImageDrmFormatModifierInfoEXT{SType: vk.StructureTypePhysicalDeviceImageDRMFormatModifierInfoEXT, DrmFormatModifier: modifier, SharingMode: vk.SharingModeExclusive}
126+ ext := vk.PhysicalDeviceExternalImageFormatInfo{SType: vk.StructureTypePhysicalDeviceExternalImageFormatInfo, Next: unsafe.Pointer(&mod), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
127+ info := vk.PhysicalDeviceImageFormatInfo2{SType: vk.StructureTypePhysicalDeviceImageFormatInfo2, Next: unsafe.Pointer(&ext), Format: format, Type: vk.ImageType2d, Tiling: vk.ImageTilingDRMFormatModifierEXT, Usage: vk.ImageUsageTransferSrcBit}
128+ extOut := vk.ExternalImageFormatProperties{SType: vk.StructureTypeExternalImageFormatProperties}
129+ out := vk.ImageFormatProperties2{SType: vk.StructureTypeImageFormatProperties2, Next: unsafe.Pointer(&extOut)}
130+ if r.id.GetPhysicalDeviceImageFormatProperties2(physical, &info, &out) != vk.Success {
131+ return false
132+ }
133+ return extOut.ExternalMemoryProperties.ExternalMemoryFeatures&vk.ExternalMemoryFeatureImportableBit != 0
134+}
135+
136+// DMABuf reports the formats this renderer imports.
137+func (r *Renderer) DMABuf() ports.DMABufSupport { return r.dmabuf }
138+
139+// imported is a client dmabuf bound to a VkImage.
140+type imported struct {
141+ image vk.Image
142+ memory vk.DeviceMemory
143+ fd int // our duplicate of plane 0, for implicit sync
144+ // last is the frame that drew it, for eviction.
145+ last uint64
146+}
147+
148+// importTTL is how many frames an import survives undrawn: clients cycle
149+// through 2-4 buffers, and a destroyed buffer is freed shortly after.
150+const importTTL = 120
151+
152+// importDMABuf returns the image of a client buffer, importing it once.
153+func (r *Renderer) importDMABuf(b *ports.DMABuf) (*imported, error) {
154+ if im := r.imports[b.ID]; im != nil {
155+ im.last = r.frame
156+ return im, nil
157+ }
158+ if len(r.dmabuf.Formats) == 0 {
159+ return nil, fmt.Errorf("dmabuf unsupported")
160+ }
161+ format, _, ok := vkFormat(b.Format)
162+ if !ok || len(b.Planes) != 1 || b.Width <= 0 || b.Height <= 0 {
163+ return nil, fmt.Errorf("dmabuf format %#x with %d planes unsupported", b.Format, len(b.Planes))
164+ }
165+ fd, err := dupFile(b.Planes[0].File)
166+ if err != nil {
167+ return nil, err
168+ }
169+ im := &imported{fd: fd, last: r.frame}
170+ ok = false
171+ defer func() {
172+ if !ok {
173+ r.release(im)
174+ }
175+ }()
176+ d := r.dd
177+ layout := vk.SubresourceLayout{Offset: vk.DeviceSize(b.Planes[0].Offset), RowPitch: vk.DeviceSize(b.Planes[0].Stride)}
178+ explicit := vk.ImageDrmFormatModifierExplicitCreateInfoEXT{SType: vk.StructureTypeImageDRMFormatModifierExplicitCreateInfoEXT, DrmFormatModifier: b.Modifier, DrmFormatModifierPlaneCount: 1, PlaneLayouts: &layout}
179+ external := vk.ExternalMemoryImageCreateInfo{SType: vk.StructureTypeExternalMemoryImageCreateInfo, Next: unsafe.Pointer(&explicit), HandleTypes: vk.ExternalMemoryHandleTypeDMABUFBitEXT}
180+ 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.ImageUsageTransferSrcBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
181+ if err := checked("vkCreateImage(dmabuf)", d.CreateImage(r.device, &ii, nil, &im.image)); err != nil {
182+ return nil, err
183+ }
184+ fdProps := vk.MemoryFdPropertiesKHR{SType: vk.StructureTypeMemoryFDPropertiesKHR}
185+ if err := checked("vkGetMemoryFdPropertiesKHR", d.GetMemoryFdPropertiesKHR(r.device, vk.ExternalMemoryHandleTypeDMABUFBitEXT, int32(fd), &fdProps)); err != nil {
186+ return nil, err
187+ }
188+ var req vk.MemoryRequirements
189+ d.GetImageMemoryRequirements(r.device, im.image, &req)
190+ kind, err := r.findMemoryType(req.MemoryTypeBits&fdProps.MemoryTypeBits, 0)
191+ if err != nil {
192+ return nil, err
193+ }
194+ // Vulkan owns the fd it imports on success: hand it its own duplicate.
195+ memFD, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
196+ if err != nil {
197+ return nil, err
198+ }
199+ dedicated := vk.MemoryDedicatedAllocateInfo{SType: vk.StructureTypeMemoryDedicatedAllocateInfo, Image: im.image}
200+ imp := vk.ImportMemoryFdInfoKHR{SType: vk.StructureTypeImportMemoryFDInfoKHR, Next: unsafe.Pointer(&dedicated), HandleType: vk.ExternalMemoryHandleTypeDMABUFBitEXT, Fd: int32(memFD)}
201+ alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, Next: unsafe.Pointer(&imp), AllocationSize: req.Size, MemoryTypeIndex: kind}
202+ if err := checked("vkAllocateMemory(dmabuf)", d.AllocateMemory(r.device, &alloc, nil, &im.memory)); err != nil {
203+ unix.Close(memFD)
204+ return nil, err
205+ }
206+ if err := checked("vkBindImageMemory(dmabuf)", d.BindImageMemory(r.device, im.image, im.memory, 0)); err != nil {
207+ return nil, err
208+ }
209+ ok = true
210+ r.imports[b.ID] = im
211+ return im, nil
212+}
213+
214+// dupFile duplicates the descriptor while the file is held open, so a close
215+// on the wayland goroutine cannot swap it for a reused number.
216+func dupFile(f *os.File) (int, error) {
217+ if f == nil {
218+ return -1, fmt.Errorf("dmabuf plane without file")
219+ }
220+ raw, err := f.SyscallConn()
221+ if err != nil {
222+ return -1, err
223+ }
224+ fd, dupErr := -1, error(nil)
225+ if err := raw.Control(func(v uintptr) { fd, dupErr = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }); err != nil {
226+ return -1, err
227+ }
228+ return fd, dupErr
229+}
230+
231+func (r *Renderer) release(im *imported) {
232+ if im.image != 0 {
233+ r.dd.DestroyImage(r.device, im.image, nil)
234+ }
235+ if im.memory != 0 {
236+ r.dd.FreeMemory(r.device, im.memory, nil)
237+ }
238+ if im.fd >= 0 {
239+ unix.Close(im.fd)
240+ }
241+}
242+
243+// dropUnused frees imports not drawn for importTTL frames: the client moved
244+// on to other buffers or went away. Called after a frame completes.
245+func (r *Renderer) dropUnused() {
246+ r.frame++
247+ for id, im := range r.imports {
248+ if r.frame-im.last > importTTL {
249+ r.release(im)
250+ delete(r.imports, id)
251+ }
252+ }
253+}
254+
255+// dmaBufSync is struct dma_buf_export_sync_file.
256+type dmaBufSync struct {
257+ flags uint32
258+ fd int32
259+}
260+
261+// DMA_BUF_IOCTL_EXPORT_SYNC_FILE = _IOWR('b', 2, struct dma_buf_export_sync_file)
262+const ioctlExportSyncFile = 0xc0086202
263+const dmaBufSyncRead = 1
264+
265+// readFence returns a semaphore signalled when the client's pending writes
266+// to the buffer finish, or 0 when the kernel cannot export them (older than
267+// 5.20): the buffer is then read as is.
268+func (r *Renderer) readFence(im *imported) vk.Semaphore {
269+ arg := dmaBufSync{flags: dmaBufSyncRead, fd: -1}
270+ if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(im.fd), ioctlExportSyncFile, uintptr(unsafe.Pointer(&arg))); errno != 0 || arg.fd < 0 {
271+ return 0
272+ }
273+ var sem vk.Semaphore
274+ si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
275+ if r.dd.CreateSemaphore(r.device, &si, nil, &sem) != vk.Success {
276+ unix.Close(int(arg.fd))
277+ return 0
278+ }
279+ imp := vk.ImportSemaphoreFdInfoKHR{SType: vk.StructureTypeImportSemaphoreFDInfoKHR, Semaphore: sem, Flags: vk.SemaphoreImportTemporaryBit, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit, Fd: arg.fd}
280+ if r.dd.ImportSemaphoreFdKHR(r.device, &imp) != vk.Success {
281+ unix.Close(int(arg.fd))
282+ r.dd.DestroySemaphore(r.device, sem, nil)
283+ return 0
284+ }
285+ return sem
286+}
287+
288+// Probe reports the dmabuf formats of the device renderers use, or none
289+// when Vulkan or the extensions are missing.
290+func Probe() ports.DMABufSupport {
291+ r, err := New(1, 1)
292+ if err != nil {
293+ return ports.DMABufSupport{}
294+ }
295+ defer r.Close()
296+ return r.DMABuf()
297+}
A internal/adapters/vulkan/dmabuf_test.go +108 -0
@@ -0,0 +1,108 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "os"
6+ "slices"
7+ "testing"
8+ "unsafe"
9+
10+ "github.com/bnema/nefertty/internal/ports"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU
15+// driver imports, like a client's.
16+func udmabuf(t *testing.T, w, h int, fill func(x, y int) [4]byte) *os.File {
17+ t.Helper()
18+ dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0)
19+ if err != nil {
20+ t.Skipf("udmabuf unavailable: %v", err)
21+ }
22+ defer dev.Close()
23+ size := (w*h*4 + os.Getpagesize() - 1) / os.Getpagesize() * os.Getpagesize()
24+ mem, err := unix.MemfdCreate("dmabuf-test", unix.MFD_ALLOW_SEALING)
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ defer unix.Close(mem)
29+ if err := unix.Ftruncate(mem, int64(size)); err != nil {
30+ t.Fatal(err)
31+ }
32+ data, err := unix.Mmap(mem, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
33+ if err != nil {
34+ t.Fatal(err)
35+ }
36+ for y := range h {
37+ for x := range w {
38+ p := fill(x, y)
39+ copy(data[(y*w+x)*4:], p[:])
40+ }
41+ }
42+ _ = unix.Munmap(data)
43+ if _, err := unix.FcntlInt(uintptr(mem), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK); err != nil {
44+ t.Fatal(err)
45+ }
46+ // struct udmabuf_create { u32 memfd; u32 flags; u64 offset; u64 size; }
47+ arg := struct {
48+ memfd, flags uint32
49+ offset, size uint64
50+ }{memfd: uint32(mem), flags: 1 /* CLOEXEC */, size: uint64(size)}
51+ const udmabufCreate = 0x40187542 // _IOW('u', 0x42, struct udmabuf_create)
52+ fd, _, errno := unix.Syscall(unix.SYS_IOCTL, dev.Fd(), udmabufCreate, uintptr(unsafe.Pointer(&arg)))
53+ if errno != 0 {
54+ t.Skipf("udmabuf create: %v", errno)
55+ }
56+ f := os.NewFile(fd, "dmabuf")
57+ t.Cleanup(func() { f.Close() })
58+ return f
59+}
60+
61+// A linear ARGB8888 dmabuf is imported and drawn like wl_shm pixels, and
62+// the import is freed once no frame draws it.
63+func TestRendererImportsDMABuf(t *testing.T) {
64+ r, err := New(80, 32)
65+ if err != nil {
66+ t.Skipf("Vulkan unavailable: %v", err)
67+ }
68+ defer r.Close()
69+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
70+ if !slices.Contains(r.DMABuf().Formats, linear) {
71+ t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
72+ }
73+ // Left half red, right half blue (B,G,R,A in memory). 64 pixels wide:
74+ // drivers want linear rows aligned (256 bytes on AMD).
75+ f := udmabuf(t, 64, 16, func(x, _ int) [4]byte {
76+ if x < 32 {
77+ return [4]byte{0, 0, 255, 255}
78+ }
79+ return [4]byte{255, 0, 0, 255}
80+ })
81+ buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
82+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
83+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}
84+ if err := r.Render(scene, contents); err != nil {
85+ t.Fatal(err)
86+ }
87+ px := r.Pixels()
88+ for _, c := range []struct {
89+ x, y int
90+ want color.RGBA
91+ }{{4, 4, color.RGBA{255, 0, 0, 255}}, {35, 19, color.RGBA{255, 0, 0, 255}}, {36, 4, color.RGBA{0, 0, 255, 255}}, {67, 19, color.RGBA{0, 0, 255, 255}}, {2, 2, color.RGBA{0, 0, 0, 255}}, {70, 10, color.RGBA{0, 0, 0, 255}}} {
92+ if got := px.At(c.x, c.y); got != c.want {
93+ t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
94+ }
95+ }
96+ if len(r.imports) != 1 {
97+ t.Fatalf("imports %d", len(r.imports))
98+ }
99+ // Frames without the buffer: its import is released after importTTL.
100+ for range importTTL + 1 {
101+ if err := r.Render(scene, nil); err != nil {
102+ t.Fatal(err)
103+ }
104+ }
105+ if len(r.imports) != 0 {
106+ t.Fatalf("import kept: %d", len(r.imports))
107+ }
108+}
@@ -0,0 +1,108 @@
1+package vulkan
2+
3+import (
4+ "image/color"
5+ "os"
6+ "slices"
7+ "testing"
8+ "unsafe"
9+
10+ "github.com/bnema/nefertty/internal/ports"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU
15+// driver imports, like a client's.
16+func udmabuf(t *testing.T, w, h int, fill func(x, y int) [4]byte) *os.File {
17+ t.Helper()
18+ dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0)
19+ if err != nil {
20+ t.Skipf("udmabuf unavailable: %v", err)
21+ }
22+ defer dev.Close()
23+ size := (w*h*4 + os.Getpagesize() - 1) / os.Getpagesize() * os.Getpagesize()
24+ mem, err := unix.MemfdCreate("dmabuf-test", unix.MFD_ALLOW_SEALING)
25+ if err != nil {
26+ t.Fatal(err)
27+ }
28+ defer unix.Close(mem)
29+ if err := unix.Ftruncate(mem, int64(size)); err != nil {
30+ t.Fatal(err)
31+ }
32+ data, err := unix.Mmap(mem, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
33+ if err != nil {
34+ t.Fatal(err)
35+ }
36+ for y := range h {
37+ for x := range w {
38+ p := fill(x, y)
39+ copy(data[(y*w+x)*4:], p[:])
40+ }
41+ }
42+ _ = unix.Munmap(data)
43+ if _, err := unix.FcntlInt(uintptr(mem), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK); err != nil {
44+ t.Fatal(err)
45+ }
46+ // struct udmabuf_create { u32 memfd; u32 flags; u64 offset; u64 size; }
47+ arg := struct {
48+ memfd, flags uint32
49+ offset, size uint64
50+ }{memfd: uint32(mem), flags: 1 /* CLOEXEC */, size: uint64(size)}
51+ const udmabufCreate = 0x40187542 // _IOW('u', 0x42, struct udmabuf_create)
52+ fd, _, errno := unix.Syscall(unix.SYS_IOCTL, dev.Fd(), udmabufCreate, uintptr(unsafe.Pointer(&arg)))
53+ if errno != 0 {
54+ t.Skipf("udmabuf create: %v", errno)
55+ }
56+ f := os.NewFile(fd, "dmabuf")
57+ t.Cleanup(func() { f.Close() })
58+ return f
59+}
60+
61+// A linear ARGB8888 dmabuf is imported and drawn like wl_shm pixels, and
62+// the import is freed once no frame draws it.
63+func TestRendererImportsDMABuf(t *testing.T) {
64+ r, err := New(80, 32)
65+ if err != nil {
66+ t.Skipf("Vulkan unavailable: %v", err)
67+ }
68+ defer r.Close()
69+ linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
70+ if !slices.Contains(r.DMABuf().Formats, linear) {
71+ t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
72+ }
73+ // Left half red, right half blue (B,G,R,A in memory). 64 pixels wide:
74+ // drivers want linear rows aligned (256 bytes on AMD).
75+ f := udmabuf(t, 64, 16, func(x, _ int) [4]byte {
76+ if x < 32 {
77+ return [4]byte{0, 0, 255, 255}
78+ }
79+ return [4]byte{255, 0, 0, 255}
80+ })
81+ buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
82+ scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
83+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}
84+ if err := r.Render(scene, contents); err != nil {
85+ t.Fatal(err)
86+ }
87+ px := r.Pixels()
88+ for _, c := range []struct {
89+ x, y int
90+ want color.RGBA
91+ }{{4, 4, color.RGBA{255, 0, 0, 255}}, {35, 19, color.RGBA{255, 0, 0, 255}}, {36, 4, color.RGBA{0, 0, 255, 255}}, {67, 19, color.RGBA{0, 0, 255, 255}}, {2, 2, color.RGBA{0, 0, 0, 255}}, {70, 10, color.RGBA{0, 0, 0, 255}}} {
92+ if got := px.At(c.x, c.y); got != c.want {
93+ t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
94+ }
95+ }
96+ if len(r.imports) != 1 {
97+ t.Fatalf("imports %d", len(r.imports))
98+ }
99+ // Frames without the buffer: its import is released after importTTL.
100+ for range importTTL + 1 {
101+ if err := r.Render(scene, nil); err != nil {
102+ t.Fatal(err)
103+ }
104+ }
105+ if len(r.imports) != 0 {
106+ t.Fatalf("import kept: %d", len(r.imports))
107+ }
108+}
M internal/adapters/vulkan/renderer.go +113 -4
@@ -6,6 +6,7 @@ import (
66 "math"
77 "os"
88 "runtime"
9+ "slices"
910 "strconv"
1011 "unsafe"
1112
@@ -36,6 +37,11 @@ type Renderer struct {
3637 fence vk.Fence
3738 layout vk.ImageLayout
3839 memory vk.PhysicalDeviceMemoryProperties
40+ family uint32
41+ // dmabuf is what clients may send; imports are their buffers by ID.
42+ dmabuf ports.DMABufSupport
43+ imports map[uint64]*imported
44+ frame uint64
3945 }
4046
4147 func checked(name string, result vk.Result) error {
@@ -49,7 +55,7 @@ func New(width, height int) (r *Renderer, err error) {
4955 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
5056 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
5157 }
52- r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined}
58+ r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}}
5359 defer func() {
5460 if err != nil {
5561 r.Close()
@@ -123,10 +129,22 @@ func New(width, height int) (r *Renderer, err error) {
123129 return
124130 }
125131 r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
132+ r.family = family
126133 priority := float32(1)
127134 qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
128135 dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1}
129136 di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi}
137+ // dmabuf import needs every extension; without them clients use wl_shm.
138+ var extNames [][]byte
139+ var extPtrs []*byte
140+ if r.hasExtensions(physical) {
141+ for _, e := range deviceExtensions {
142+ extNames = append(extNames, append([]byte(e), 0))
143+ extPtrs = append(extPtrs, &extNames[len(extNames)-1][0])
144+ }
145+ di.EnabledExtensionCount = uint32(len(extPtrs))
146+ di.PpEnabledExtensionNames = &extPtrs[0]
147+ }
130148 if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil {
131149 return
132150 }
@@ -135,6 +153,14 @@ func New(width, height int) (r *Renderer, err error) {
135153 err = fmt.Errorf("LoadDeviceDispatch: %w", err)
136154 return
137155 }
156+ runtime.KeepAlive(extNames)
157+ runtime.KeepAlive(extPtrs)
158+ if len(extPtrs) > 0 {
159+ // Clients need the render node to allocate on the right GPU.
160+ if sup := r.probeDMABuf(physical); sup.Device != 0 {
161+ r.dmabuf = sup
162+ }
163+ }
138164 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
139165 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
140166 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
@@ -332,6 +358,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
332358 dst, src image.Rectangle
333359 color [3]uint8
334360 opaque bool
361+ // dma is a client GPU buffer, blitted instead of copied from staging.
362+ dma *imported
335363 }
336364 var uploads []upload
337365 used := 0
@@ -350,6 +378,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
350378 if rect.Empty() || src.Empty() {
351379 return
352380 }
381+ if content.DMABuf != nil {
382+ im, err := r.importDMABuf(content.DMABuf)
383+ if err != nil {
384+ // The window shows its background until a buffer imports.
385+ return
386+ }
387+ uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})
388+ return
389+ }
353390 uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})
354391 used += rect.Dx() * rect.Dy() * 4
355392 }
@@ -359,7 +396,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
359396 if lw <= 0 || lh <= 0 {
360397 lw, lh = content.Width, content.Height
361398 }
362- if content.Pixels == nil || lw <= 0 || lh <= 0 || content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4 {
399+ if content.Empty() || lw <= 0 || lh <= 0 {
400+ return
401+ }
402+ if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) {
363403 return
364404 }
365405 vw, vh := min(lw, w), min(lh, h)
@@ -416,7 +456,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
416456 cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b
417457 body := physRect(cx, cy, cw, ch)
418458 content := contents[w.ID]
419- if content.Pixels == nil {
459+ if content.Empty() {
420460 add(body, windowColor(w.ID))
421461 } else {
422462 add(body, parseColor(s.Background))
@@ -445,6 +485,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
445485 }
446486 data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
447487 for _, u := range uploads {
488+ if u.dma != nil {
489+ continue
490+ }
448491 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
449492 for row := 0; row < u.rect.Dy(); row++ {
450493 line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
@@ -502,14 +545,54 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
502545 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
503546 barrier.NewLayout = vk.ImageLayoutTransferDstOptimal
504547 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
548+ // Client buffers come from the foreign queue family (their driver) and
549+ // go back to it after the frame, so the next frame acquires them again.
550+ var dmas []*imported
551+ for _, u := range uploads {
552+ if u.dma != nil && !slices.Contains(dmas, u.dma) {
553+ dmas = append(dmas, u.dma)
554+ }
555+ }
556+ ownership := func(acquire bool) {
557+ for _, im := range dmas {
558+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: rangeInfo}
559+ if acquire {
560+ // GENERAL, never UNDEFINED: the client's contents must survive.
561+ b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutTransferSrcOptimal
562+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
563+ b.DstAccessMask = vk.AccessTransferReadBit
564+ } else {
565+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
566+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
567+ b.SrcAccessMask = vk.AccessTransferReadBit
568+ }
569+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
570+ }
571+ }
572+ ownership(true)
505573 for i, f := range uploads {
506574 if i > 0 {
507575 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
508576 }
509577 rect := f.rect
578+ if f.dma != nil {
579+ // Map the visible part of dst back onto the buffer.
580+ sx := func(x int) int32 { return int32(f.src.Min.X + (x-f.dst.Min.X)*f.src.Dx()/f.dst.Dx()) }
581+ sy := func(y int) int32 { return int32(f.src.Min.Y + (y-f.dst.Min.Y)*f.src.Dy()/f.dst.Dy()) }
582+ layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
583+ blit := vk.ImageBlit{
584+ SrcSubresource: layers,
585+ SrcOffsets: [2]vk.Offset3D{{X: sx(rect.Min.X), Y: sy(rect.Min.Y)}, {X: sx(rect.Max.X), Y: sy(rect.Max.Y), Z: 1}},
586+ DstSubresource: layers,
587+ DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},
588+ }
589+ d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, r.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
590+ continue
591+ }
510592 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
511593 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
512594 }
595+ ownership(false)
513596 barrier.DstAccessMask = vk.AccessTransferReadBit
514597 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
515598 barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
@@ -519,14 +602,35 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
519602 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
520603 return err
521604 }
605+ // Implicit sync: wait for the client's GPU writes to each buffer.
606+ var waits []vk.Semaphore
607+ for _, im := range dmas {
608+ if sem := r.readFence(im); sem != 0 {
609+ waits = append(waits, sem)
610+ }
611+ }
612+ defer func() {
613+ for _, sem := range waits {
614+ d.DestroySemaphore(r.device, sem, nil)
615+ }
616+ }()
522617 submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
618+ stages := make([]vk.PipelineStageFlags, len(waits))
619+ if len(waits) > 0 {
620+ for i := range stages {
621+ stages[i] = vk.PipelineStageTransferBit
622+ }
623+ submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
624+ }
523625 if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
524626 return err
525627 }
628+ runtime.KeepAlive(stages)
526629 r.layout = vk.ImageLayoutTransferSrcOptimal
527630 if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
528631 return err
529632 }
633+ r.dropUnused()
530634 return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
531635 }
532636
@@ -537,7 +641,8 @@ func (r *Renderer) Pixels() *image.RGBA {
537641 }
538642 src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))
539643 for i := 0; i < len(src); i += 4 {
540- out.Pix[i], out.Pix[i+1], out.Pix[i+2], out.Pix[i+3] = src[i+2], src[i+1], src[i], src[i+3]
644+ // The output is opaque: x-format client buffers leave alpha undefined.
645+ out.Pix[i], out.Pix[i+1], out.Pix[i+2], out.Pix[i+3] = src[i+2], src[i+1], src[i], 255
541646 }
542647 return out
543648 }
@@ -564,6 +669,10 @@ func (r *Renderer) Close() {
564669 if r.device != 0 {
565670 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
566671 }
672+ for id, im := range r.imports {
673+ r.release(im)
674+ delete(r.imports, id)
675+ }
567676 if r.fence != 0 {
568677 d.DestroyFence(r.device, r.fence, nil)
569678 r.fence = 0
@@ -6,6 +6,7 @@ import (
6 "math"6 "math"
7 "os"7 "os"
8 "runtime"8 "runtime"
9+ "slices"
9 "strconv"10 "strconv"
10 "unsafe"11 "unsafe"
11 12
@@ -36,6 +37,11 @@ type Renderer struct {
36 fence vk.Fence37 fence vk.Fence
37 layout vk.ImageLayout38 layout vk.ImageLayout
38 memory vk.PhysicalDeviceMemoryProperties39 memory vk.PhysicalDeviceMemoryProperties
40+ family uint32
41+ // dmabuf is what clients may send; imports are their buffers by ID.
42+ dmabuf ports.DMABufSupport
43+ imports map[uint64]*imported
44+ frame uint64
39 }45 }
40 46
41 func checked(name string, result vk.Result) error {47 func checked(name string, result vk.Result) error {
@@ -49,7 +55,7 @@ func New(width, height int) (r *Renderer, err error) {
49 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {55 if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
50 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)56 return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
51 }57 }
52- r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined}58+ r = &Renderer{width: width, height: height, layout: vk.ImageLayoutUndefined, imports: map[uint64]*imported{}}
53 defer func() {59 defer func() {
54 if err != nil {60 if err != nil {
55 r.Close()61 r.Close()
@@ -123,10 +129,22 @@ func New(width, height int) (r *Renderer, err error) {
123 return129 return
124 }130 }
125 r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)131 r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
132+ r.family = family
126 priority := float32(1)133 priority := float32(1)
127 qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}134 qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
128 dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1}135 dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1}
129 di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi}136 di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi}
137+ // dmabuf import needs every extension; without them clients use wl_shm.
138+ var extNames [][]byte
139+ var extPtrs []*byte
140+ if r.hasExtensions(physical) {
141+ for _, e := range deviceExtensions {
142+ extNames = append(extNames, append([]byte(e), 0))
143+ extPtrs = append(extPtrs, &extNames[len(extNames)-1][0])
144+ }
145+ di.EnabledExtensionCount = uint32(len(extPtrs))
146+ di.PpEnabledExtensionNames = &extPtrs[0]
147+ }
130 if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil {148 if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil {
131 return149 return
132 }150 }
@@ -135,6 +153,14 @@ func New(width, height int) (r *Renderer, err error) {
135 err = fmt.Errorf("LoadDeviceDispatch: %w", err)153 err = fmt.Errorf("LoadDeviceDispatch: %w", err)
136 return154 return
137 }155 }
156+ runtime.KeepAlive(extNames)
157+ runtime.KeepAlive(extPtrs)
158+ if len(extPtrs) > 0 {
159+ // Clients need the render node to allocate on the right GPU.
160+ if sup := r.probeDMABuf(physical); sup.Device != 0 {
161+ r.dmabuf = sup
162+ }
163+ }
138 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)164 r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
139 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}165 extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
140 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}166 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: extent, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageTransferSrcBit | vk.ImageUsageTransferDstBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
@@ -332,6 +358,8 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
332 dst, src image.Rectangle358 dst, src image.Rectangle
333 color [3]uint8359 color [3]uint8
334 opaque bool360 opaque bool
361+ // dma is a client GPU buffer, blitted instead of copied from staging.
362+ dma *imported
335 }363 }
336 var uploads []upload364 var uploads []upload
337 used := 0365 used := 0
@@ -350,6 +378,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
350 if rect.Empty() || src.Empty() {378 if rect.Empty() || src.Empty() {
351 return379 return
352 }380 }
381+ if content.DMABuf != nil {
382+ im, err := r.importDMABuf(content.DMABuf)
383+ if err != nil {
384+ // The window shows its background until a buffer imports.
385+ return
386+ }
387+ uploads = append(uploads, upload{rect: rect, dst: dst, src: src, dma: im})
388+ return
389+ }
353 uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})390 uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src})
354 used += rect.Dx() * rect.Dy() * 4391 used += rect.Dx() * rect.Dy() * 4
355 }392 }
@@ -359,7 +396,10 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
359 if lw <= 0 || lh <= 0 {396 if lw <= 0 || lh <= 0 {
360 lw, lh = content.Width, content.Height397 lw, lh = content.Width, content.Height
361 }398 }
362- if content.Pixels == nil || lw <= 0 || lh <= 0 || content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4 {399+ if content.Empty() || lw <= 0 || lh <= 0 {
400+ return
401+ }
402+ if content.DMABuf == nil && (content.Stride < content.Width*4 || len(content.Pixels) < (content.Height-1)*content.Stride+content.Width*4) {
363 return403 return
364 }404 }
365 vw, vh := min(lw, w), min(lh, h)405 vw, vh := min(lw, w), min(lh, h)
@@ -416,7 +456,7 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
416 cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b456 cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b
417 body := physRect(cx, cy, cw, ch)457 body := physRect(cx, cy, cw, ch)
418 content := contents[w.ID]458 content := contents[w.ID]
419- if content.Pixels == nil {459+ if content.Empty() {
420 add(body, windowColor(w.ID))460 add(body, windowColor(w.ID))
421 } else {461 } else {
422 add(body, parseColor(s.Background))462 add(body, parseColor(s.Background))
@@ -445,6 +485,9 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
445 }485 }
446 data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)486 data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize)
447 for _, u := range uploads {487 for _, u := range uploads {
488+ if u.dma != nil {
489+ continue
490+ }
448 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]491 dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4]
449 for row := 0; row < u.rect.Dy(); row++ {492 for row := 0; row < u.rect.Dy(); row++ {
450 line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]493 line := dst[row*u.rect.Dx()*4 : (row+1)*u.rect.Dx()*4]
@@ -502,14 +545,54 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
502 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal545 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
503 barrier.NewLayout = vk.ImageLayoutTransferDstOptimal546 barrier.NewLayout = vk.ImageLayoutTransferDstOptimal
504 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)547 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
548+ // Client buffers come from the foreign queue family (their driver) and
549+ // go back to it after the frame, so the next frame acquires them again.
550+ var dmas []*imported
551+ for _, u := range uploads {
552+ if u.dma != nil && !slices.Contains(dmas, u.dma) {
553+ dmas = append(dmas, u.dma)
554+ }
555+ }
556+ ownership := func(acquire bool) {
557+ for _, im := range dmas {
558+ b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: rangeInfo}
559+ if acquire {
560+ // GENERAL, never UNDEFINED: the client's contents must survive.
561+ b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutTransferSrcOptimal
562+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
563+ b.DstAccessMask = vk.AccessTransferReadBit
564+ } else {
565+ b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
566+ b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
567+ b.SrcAccessMask = vk.AccessTransferReadBit
568+ }
569+ d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
570+ }
571+ }
572+ ownership(true)
505 for i, f := range uploads {573 for i, f := range uploads {
506 if i > 0 {574 if i > 0 {
507 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)575 d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier)
508 }576 }
509 rect := f.rect577 rect := f.rect
578+ if f.dma != nil {
579+ // Map the visible part of dst back onto the buffer.
580+ sx := func(x int) int32 { return int32(f.src.Min.X + (x-f.dst.Min.X)*f.src.Dx()/f.dst.Dx()) }
581+ sy := func(y int) int32 { return int32(f.src.Min.Y + (y-f.dst.Min.Y)*f.src.Dy()/f.dst.Dy()) }
582+ layers := vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}
583+ blit := vk.ImageBlit{
584+ SrcSubresource: layers,
585+ SrcOffsets: [2]vk.Offset3D{{X: sx(rect.Min.X), Y: sy(rect.Min.Y)}, {X: sx(rect.Max.X), Y: sy(rect.Max.Y), Z: 1}},
586+ DstSubresource: layers,
587+ DstOffsets: [2]vk.Offset3D{{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, {X: int32(rect.Max.X), Y: int32(rect.Max.Y), Z: 1}},
588+ }
589+ d.CmdBlitImage(r.command, f.dma.image, vk.ImageLayoutTransferSrcOptimal, r.image, vk.ImageLayoutTransferDstOptimal, 1, &blit, vk.FilterNearest)
590+ continue
591+ }
510 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}592 region := vk.BufferImageCopy{BufferOffset: vk.DeviceSize(f.offset), ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageOffset: vk.Offset3D{X: int32(rect.Min.X), Y: int32(rect.Min.Y)}, ImageExtent: vk.Extent3D{Width: uint32(rect.Dx()), Height: uint32(rect.Dy()), Depth: 1}}
511 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)593 d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, &region)
512 }594 }
595+ ownership(false)
513 barrier.DstAccessMask = vk.AccessTransferReadBit596 barrier.DstAccessMask = vk.AccessTransferReadBit
514 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal597 barrier.OldLayout = vk.ImageLayoutTransferDstOptimal
515 barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal598 barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal
@@ -519,14 +602,35 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa
519 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {602 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil {
520 return err603 return err
521 }604 }
605+ // Implicit sync: wait for the client's GPU writes to each buffer.
606+ var waits []vk.Semaphore
607+ for _, im := range dmas {
608+ if sem := r.readFence(im); sem != 0 {
609+ waits = append(waits, sem)
610+ }
611+ }
612+ defer func() {
613+ for _, sem := range waits {
614+ d.DestroySemaphore(r.device, sem, nil)
615+ }
616+ }()
522 submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}617 submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &r.command}
618+ stages := make([]vk.PipelineStageFlags, len(waits))
619+ if len(waits) > 0 {
620+ for i := range stages {
621+ stages[i] = vk.PipelineStageTransferBit
622+ }
623+ submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
624+ }
523 if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {625 if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil {
524 return err626 return err
525 }627 }
628+ runtime.KeepAlive(stages)
526 r.layout = vk.ImageLayoutTransferSrcOptimal629 r.layout = vk.ImageLayoutTransferSrcOptimal
527 if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {630 if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil {
528 return err631 return err
529 }632 }
633+ r.dropUnused()
530 return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))634 return checked("vkResetFences", d.ResetFences(r.device, 1, &r.fence))
531 }635 }
532 636
@@ -537,7 +641,8 @@ func (r *Renderer) Pixels() *image.RGBA {
537 }641 }
538 src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))642 src := unsafe.Slice((*byte)(r.mapped), len(out.Pix))
539 for i := 0; i < len(src); i += 4 {643 for i := 0; i < len(src); i += 4 {
540- out.Pix[i], out.Pix[i+1], out.Pix[i+2], out.Pix[i+3] = src[i+2], src[i+1], src[i], src[i+3]644+ // The output is opaque: x-format client buffers leave alpha undefined.
645+ out.Pix[i], out.Pix[i+1], out.Pix[i+2], out.Pix[i+3] = src[i+2], src[i+1], src[i], 255
541 }646 }
542 return out647 return out
543 }648 }
@@ -564,6 +669,10 @@ func (r *Renderer) Close() {
564 if r.device != 0 {669 if r.device != 0 {
565 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))670 _ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
566 }671 }
672+ for id, im := range r.imports {
673+ r.release(im)
674+ delete(r.imports, id)
675+ }
567 if r.fence != 0 {676 if r.fence != 0 {
568 d.DestroyFence(r.device, r.fence, nil)677 d.DestroyFence(r.device, r.fence, nil)
569 r.fence = 0678 r.fence = 0
A internal/adapters/wayland/dmabuf.go +300 -0
@@ -0,0 +1,300 @@
1+package wayland
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "slices"
8+
9+ "github.com/bnema/nefertty/internal/ports"
10+ "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
11+ "github.com/bnema/purego-libwayland/protocol/wayland"
12+ "github.com/bnema/purego-libwayland/server"
13+ "golang.org/x/sys/unix"
14+)
15+
16+// linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.
17+// The formats are those the renderer imports (ports.DMABufSupport); without
18+// any, the global is not advertised and clients use wl_shm.
19+
20+const dmabufVersion = 4
21+
22+// maxPlanes is the protocol limit (DRM planes).
23+const maxPlanes = 4
24+
25+func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error {
26+ if len(sup.Formats) == 0 {
27+ return nil
28+ }
29+ table, err := formatTable(sup.Formats)
30+ if err != nil {
31+ return err
32+ }
33+ s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table}
34+ return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
35+ r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
36+ if err != nil || v >= 4 {
37+ return
38+ }
39+ // Before v4 formats come as events on bind.
40+ for _, f := range sup.Formats {
41+ if v >= 3 {
42+ r.SendModifier(f.Format, uint32(f.Modifier>>32), uint32(f.Modifier))
43+ } else if f.Modifier == 0 {
44+ r.SendFormat(f.Format)
45+ }
46+ }
47+ })
48+}
49+
50+// formatTable is the shared memfd of 16-byte entries (u32 format, 4 bytes
51+// padding, u64 modifier) feedback clients map read-only.
52+func formatTable(formats []ports.DMABufFormat) (*os.File, error) {
53+ data := make([]byte, 16*len(formats))
54+ for i, f := range formats {
55+ binary.NativeEndian.PutUint32(data[i*16:], f.Format)
56+ binary.NativeEndian.PutUint64(data[i*16+8:], f.Modifier)
57+ }
58+ fd, err := unix.MemfdCreate("nefertty-dmabuf-formats", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
59+ if err != nil {
60+ return nil, err
61+ }
62+ f := os.NewFile(uintptr(fd), "dmabuf-formats")
63+ if _, err := f.Write(data); err != nil {
64+ f.Close()
65+ return nil, err
66+ }
67+ // Clients map it: it must never shrink or change.
68+ if _, err := unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL); err != nil {
69+ f.Close()
70+ return nil, err
71+ }
72+ return f, nil
73+}
74+
75+type dmabufGlobal struct {
76+ server *Server
77+ support ports.DMABufSupport
78+ table *os.File
79+ nextID uint64
80+}
81+
82+func (g *dmabufGlobal) close() {
83+ if g != nil && g.table != nil {
84+ g.table.Close()
85+ }
86+}
87+
88+func (*dmabufGlobal) Destroy(*linuxdmabuf.ZwpLinuxDmabufV1) {}
89+
90+func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
91+ p := &params{global: g}
92+ res, err := linuxdmabuf.NewZwpLinuxBufferParamsV1(r.Client(), r.Version(), id, p)
93+ if err != nil {
94+ return
95+ }
96+ p.resource = res
97+ res.OnDestroy = p.closeFiles
98+}
99+
100+func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
101+ g.feedback(r, id)
102+}
103+
104+// GetSurfaceFeedback sends the default feedback: one renderer, one tranche.
105+// Scanout tranches come with direct scanout.
106+func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, _ *wayland.Surface) {
107+ g.feedback(r, id)
108+}
109+
110+func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
111+ fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{})
112+ if err != nil {
113+ return
114+ }
115+ dev := make([]byte, 8)
116+ binary.NativeEndian.PutUint64(dev, g.support.Device)
117+ indices := make([]byte, 2*len(g.support.Formats))
118+ for i := range g.support.Formats {
119+ binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))
120+ }
121+ fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
122+ fb.SendMainDevice(dev)
123+ fb.SendTrancheTargetDevice(dev)
124+ fb.SendTrancheFlags(0)
125+ fb.SendTrancheFormats(indices)
126+ fb.SendTrancheDone()
127+ fb.SendDone()
128+}
129+
130+type feedback struct{}
131+
132+func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {}
133+
134+// params collects the planes of one buffer.
135+type params struct {
136+ global *dmabufGlobal
137+ resource *linuxdmabuf.ZwpLinuxBufferParamsV1
138+ planes [maxPlanes]*plane
139+ modifier uint64
140+ used bool
141+}
142+
143+type plane struct {
144+ file *os.File
145+ offset, stride uint32
146+}
147+
148+func (p *params) closeFiles() {
149+ for i, pl := range p.planes {
150+ if pl != nil {
151+ pl.file.Close()
152+ p.planes[i] = nil
153+ }
154+ }
155+}
156+
157+func (p *params) Destroy(*linuxdmabuf.ZwpLinuxBufferParamsV1) {}
158+
159+func (p *params) Add(r *linuxdmabuf.ZwpLinuxBufferParamsV1, fd int, idx, offset, stride, modHi, modLo uint32) {
160+ f := os.NewFile(uintptr(fd), "dmabuf")
161+ switch {
162+ case p.used:
163+ f.Close()
164+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed), "params already used")
165+ case idx >= maxPlanes:
166+ f.Close()
167+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorPlaneIdx), "plane index out of bounds")
168+ case p.planes[idx] != nil:
169+ f.Close()
170+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorPlaneSet), "plane already set")
171+ default:
172+ mod := uint64(modHi)<<32 | uint64(modLo)
173+ if p.anyPlane() && mod != p.modifier {
174+ f.Close()
175+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat), "planes with different modifiers")
176+ return
177+ }
178+ p.modifier = mod
179+ p.planes[idx] = &plane{file: f, offset: offset, stride: stride}
180+ }
181+}
182+
183+func (p *params) anyPlane() bool {
184+ return slices.ContainsFunc(p.planes[:], func(pl *plane) bool { return pl != nil })
185+}
186+
187+// build validates the planes and turns them into a buffer; ok is false after
188+// a protocol error, failed means the client gets `failed`.
189+func (p *params) build(width, height int32, format, flags uint32) (buf *dmabufBuffer, failed, ok bool) {
190+ r := p.resource
191+ if p.used {
192+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed), "params already used")
193+ return nil, false, false
194+ }
195+ p.used = true
196+ n := 0
197+ for n < maxPlanes && p.planes[n] != nil {
198+ n++
199+ }
200+ if n == 0 || slices.ContainsFunc(p.planes[n:], func(pl *plane) bool { return pl != nil }) {
201+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorIncomplete), "missing planes")
202+ return nil, false, false
203+ }
204+ if width <= 0 || height <= 0 {
205+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "invalid dimensions")
206+ return nil, false, false
207+ }
208+ // Unsupported formats, y-inverted or interlaced buffers: the client
209+ // falls back to another format or to wl_shm.
210+ want := ports.DMABufFormat{Format: format, Modifier: p.modifier}
211+ if flags != 0 || !slices.Contains(p.global.support.Formats, want) {
212+ return nil, true, true
213+ }
214+ // Supported formats are single-plane, 4 bytes per pixel.
215+ if n != 1 {
216+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorIncomplete), "format takes one plane")
217+ return nil, false, false
218+ }
219+ pl := p.planes[0]
220+ row := uint64(width) * 4
221+ end := uint64(pl.offset) + uint64(pl.stride)*uint64(height-1) + row
222+ size, err := pl.file.Seek(0, io.SeekEnd)
223+ if uint64(pl.stride) < row || (err == nil && end > uint64(size)) {
224+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds), "plane out of bounds")
225+ return nil, false, false
226+ }
227+ p.global.nextID++
228+ d := &ports.DMABuf{ID: p.global.nextID, Width: int(width), Height: int(height), Format: format, Modifier: p.modifier}
229+ for _, pl := range p.planes[:n] {
230+ d.Planes = append(d.Planes, ports.DMABufPlane{File: pl.file, Offset: pl.offset, Stride: pl.stride})
231+ }
232+ // The buffer owns the files now.
233+ p.planes = [maxPlanes]*plane{}
234+ return &dmabufBuffer{buf: d}, false, true
235+}
236+
237+func (p *params) Create(r *linuxdmabuf.ZwpLinuxBufferParamsV1, width, height int32, format, flags uint32) {
238+ b, failed, ok := p.build(width, height, format, flags)
239+ if !ok {
240+ return
241+ }
242+ if failed {
243+ r.SendFailed()
244+ return
245+ }
246+ // A server-side wl_buffer: id 0 asks libwayland to allocate one.
247+ res, err := wayland.NewBuffer(r.Client(), 1, 0, b)
248+ if err != nil {
249+ b.close()
250+ r.SendFailed()
251+ return
252+ }
253+ p.global.server.addBuffer(res, b)
254+ r.SendCreated(res)
255+}
256+
257+func (p *params) CreateImmed(r *linuxdmabuf.ZwpLinuxBufferParamsV1, id uint32, width, height int32, format, flags uint32) {
258+ b, failed, ok := p.build(width, height, format, flags)
259+ if !ok {
260+ return
261+ }
262+ if failed {
263+ // Immediate creation has no failure event: the protocol says to
264+ // raise invalid_wl_buffer.
265+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidWlBuffer), "unsupported buffer")
266+ return
267+ }
268+ res, err := wayland.NewBuffer(r.Client(), 1, id, b)
269+ if err != nil {
270+ b.close()
271+ return
272+ }
273+ p.global.server.addBuffer(res, b)
274+}
275+
276+func (*params) SetSamplingDevice(*linuxdmabuf.ZwpLinuxBufferParamsV1, []byte) {}
277+
278+// dmabufBuffer is a wl_buffer backed by client GPU memory.
279+type dmabufBuffer struct{ buf *ports.DMABuf }
280+
281+func (*dmabufBuffer) Destroy(*wayland.Buffer) {}
282+
283+func (b *dmabufBuffer) close() {
284+ for _, pl := range b.buf.Planes {
285+ pl.File.Close()
286+ }
287+}
288+
289+func (b *dmabufBuffer) size() (int, int) { return b.buf.Width, b.buf.Height }
290+
291+// content hands the renderer the buffer itself: nothing is copied.
292+func (b *dmabufBuffer) content(id ports.WindowID) (ports.SurfaceContent, bool) {
293+ opaque := b.buf.Format == fourccXRGB || b.buf.Format == fourccXBGR
294+ return ports.SurfaceContent{ID: id, Width: b.buf.Width, Height: b.buf.Height, Opaque: opaque, DMABuf: b.buf}, true
295+}
296+
297+const (
298+ fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
299+ fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
300+)
@@ -0,0 +1,300 @@
1+package wayland
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "slices"
8+
9+ "github.com/bnema/nefertty/internal/ports"
10+ "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
11+ "github.com/bnema/purego-libwayland/protocol/wayland"
12+ "github.com/bnema/purego-libwayland/server"
13+ "golang.org/x/sys/unix"
14+)
15+
16+// linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.
17+// The formats are those the renderer imports (ports.DMABufSupport); without
18+// any, the global is not advertised and clients use wl_shm.
19+
20+const dmabufVersion = 4
21+
22+// maxPlanes is the protocol limit (DRM planes).
23+const maxPlanes = 4
24+
25+func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error {
26+ if len(sup.Formats) == 0 {
27+ return nil
28+ }
29+ table, err := formatTable(sup.Formats)
30+ if err != nil {
31+ return err
32+ }
33+ s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table}
34+ return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
35+ r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
36+ if err != nil || v >= 4 {
37+ return
38+ }
39+ // Before v4 formats come as events on bind.
40+ for _, f := range sup.Formats {
41+ if v >= 3 {
42+ r.SendModifier(f.Format, uint32(f.Modifier>>32), uint32(f.Modifier))
43+ } else if f.Modifier == 0 {
44+ r.SendFormat(f.Format)
45+ }
46+ }
47+ })
48+}
49+
50+// formatTable is the shared memfd of 16-byte entries (u32 format, 4 bytes
51+// padding, u64 modifier) feedback clients map read-only.
52+func formatTable(formats []ports.DMABufFormat) (*os.File, error) {
53+ data := make([]byte, 16*len(formats))
54+ for i, f := range formats {
55+ binary.NativeEndian.PutUint32(data[i*16:], f.Format)
56+ binary.NativeEndian.PutUint64(data[i*16+8:], f.Modifier)
57+ }
58+ fd, err := unix.MemfdCreate("nefertty-dmabuf-formats", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
59+ if err != nil {
60+ return nil, err
61+ }
62+ f := os.NewFile(uintptr(fd), "dmabuf-formats")
63+ if _, err := f.Write(data); err != nil {
64+ f.Close()
65+ return nil, err
66+ }
67+ // Clients map it: it must never shrink or change.
68+ if _, err := unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL); err != nil {
69+ f.Close()
70+ return nil, err
71+ }
72+ return f, nil
73+}
74+
75+type dmabufGlobal struct {
76+ server *Server
77+ support ports.DMABufSupport
78+ table *os.File
79+ nextID uint64
80+}
81+
82+func (g *dmabufGlobal) close() {
83+ if g != nil && g.table != nil {
84+ g.table.Close()
85+ }
86+}
87+
88+func (*dmabufGlobal) Destroy(*linuxdmabuf.ZwpLinuxDmabufV1) {}
89+
90+func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
91+ p := &params{global: g}
92+ res, err := linuxdmabuf.NewZwpLinuxBufferParamsV1(r.Client(), r.Version(), id, p)
93+ if err != nil {
94+ return
95+ }
96+ p.resource = res
97+ res.OnDestroy = p.closeFiles
98+}
99+
100+func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
101+ g.feedback(r, id)
102+}
103+
104+// GetSurfaceFeedback sends the default feedback: one renderer, one tranche.
105+// Scanout tranches come with direct scanout.
106+func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, _ *wayland.Surface) {
107+ g.feedback(r, id)
108+}
109+
110+func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
111+ fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{})
112+ if err != nil {
113+ return
114+ }
115+ dev := make([]byte, 8)
116+ binary.NativeEndian.PutUint64(dev, g.support.Device)
117+ indices := make([]byte, 2*len(g.support.Formats))
118+ for i := range g.support.Formats {
119+ binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))
120+ }
121+ fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
122+ fb.SendMainDevice(dev)
123+ fb.SendTrancheTargetDevice(dev)
124+ fb.SendTrancheFlags(0)
125+ fb.SendTrancheFormats(indices)
126+ fb.SendTrancheDone()
127+ fb.SendDone()
128+}
129+
130+type feedback struct{}
131+
132+func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {}
133+
134+// params collects the planes of one buffer.
135+type params struct {
136+ global *dmabufGlobal
137+ resource *linuxdmabuf.ZwpLinuxBufferParamsV1
138+ planes [maxPlanes]*plane
139+ modifier uint64
140+ used bool
141+}
142+
143+type plane struct {
144+ file *os.File
145+ offset, stride uint32
146+}
147+
148+func (p *params) closeFiles() {
149+ for i, pl := range p.planes {
150+ if pl != nil {
151+ pl.file.Close()
152+ p.planes[i] = nil
153+ }
154+ }
155+}
156+
157+func (p *params) Destroy(*linuxdmabuf.ZwpLinuxBufferParamsV1) {}
158+
159+func (p *params) Add(r *linuxdmabuf.ZwpLinuxBufferParamsV1, fd int, idx, offset, stride, modHi, modLo uint32) {
160+ f := os.NewFile(uintptr(fd), "dmabuf")
161+ switch {
162+ case p.used:
163+ f.Close()
164+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed), "params already used")
165+ case idx >= maxPlanes:
166+ f.Close()
167+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorPlaneIdx), "plane index out of bounds")
168+ case p.planes[idx] != nil:
169+ f.Close()
170+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorPlaneSet), "plane already set")
171+ default:
172+ mod := uint64(modHi)<<32 | uint64(modLo)
173+ if p.anyPlane() && mod != p.modifier {
174+ f.Close()
175+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat), "planes with different modifiers")
176+ return
177+ }
178+ p.modifier = mod
179+ p.planes[idx] = &plane{file: f, offset: offset, stride: stride}
180+ }
181+}
182+
183+func (p *params) anyPlane() bool {
184+ return slices.ContainsFunc(p.planes[:], func(pl *plane) bool { return pl != nil })
185+}
186+
187+// build validates the planes and turns them into a buffer; ok is false after
188+// a protocol error, failed means the client gets `failed`.
189+func (p *params) build(width, height int32, format, flags uint32) (buf *dmabufBuffer, failed, ok bool) {
190+ r := p.resource
191+ if p.used {
192+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed), "params already used")
193+ return nil, false, false
194+ }
195+ p.used = true
196+ n := 0
197+ for n < maxPlanes && p.planes[n] != nil {
198+ n++
199+ }
200+ if n == 0 || slices.ContainsFunc(p.planes[n:], func(pl *plane) bool { return pl != nil }) {
201+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorIncomplete), "missing planes")
202+ return nil, false, false
203+ }
204+ if width <= 0 || height <= 0 {
205+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "invalid dimensions")
206+ return nil, false, false
207+ }
208+ // Unsupported formats, y-inverted or interlaced buffers: the client
209+ // falls back to another format or to wl_shm.
210+ want := ports.DMABufFormat{Format: format, Modifier: p.modifier}
211+ if flags != 0 || !slices.Contains(p.global.support.Formats, want) {
212+ return nil, true, true
213+ }
214+ // Supported formats are single-plane, 4 bytes per pixel.
215+ if n != 1 {
216+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorIncomplete), "format takes one plane")
217+ return nil, false, false
218+ }
219+ pl := p.planes[0]
220+ row := uint64(width) * 4
221+ end := uint64(pl.offset) + uint64(pl.stride)*uint64(height-1) + row
222+ size, err := pl.file.Seek(0, io.SeekEnd)
223+ if uint64(pl.stride) < row || (err == nil && end > uint64(size)) {
224+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds), "plane out of bounds")
225+ return nil, false, false
226+ }
227+ p.global.nextID++
228+ d := &ports.DMABuf{ID: p.global.nextID, Width: int(width), Height: int(height), Format: format, Modifier: p.modifier}
229+ for _, pl := range p.planes[:n] {
230+ d.Planes = append(d.Planes, ports.DMABufPlane{File: pl.file, Offset: pl.offset, Stride: pl.stride})
231+ }
232+ // The buffer owns the files now.
233+ p.planes = [maxPlanes]*plane{}
234+ return &dmabufBuffer{buf: d}, false, true
235+}
236+
237+func (p *params) Create(r *linuxdmabuf.ZwpLinuxBufferParamsV1, width, height int32, format, flags uint32) {
238+ b, failed, ok := p.build(width, height, format, flags)
239+ if !ok {
240+ return
241+ }
242+ if failed {
243+ r.SendFailed()
244+ return
245+ }
246+ // A server-side wl_buffer: id 0 asks libwayland to allocate one.
247+ res, err := wayland.NewBuffer(r.Client(), 1, 0, b)
248+ if err != nil {
249+ b.close()
250+ r.SendFailed()
251+ return
252+ }
253+ p.global.server.addBuffer(res, b)
254+ r.SendCreated(res)
255+}
256+
257+func (p *params) CreateImmed(r *linuxdmabuf.ZwpLinuxBufferParamsV1, id uint32, width, height int32, format, flags uint32) {
258+ b, failed, ok := p.build(width, height, format, flags)
259+ if !ok {
260+ return
261+ }
262+ if failed {
263+ // Immediate creation has no failure event: the protocol says to
264+ // raise invalid_wl_buffer.
265+ r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidWlBuffer), "unsupported buffer")
266+ return
267+ }
268+ res, err := wayland.NewBuffer(r.Client(), 1, id, b)
269+ if err != nil {
270+ b.close()
271+ return
272+ }
273+ p.global.server.addBuffer(res, b)
274+}
275+
276+func (*params) SetSamplingDevice(*linuxdmabuf.ZwpLinuxBufferParamsV1, []byte) {}
277+
278+// dmabufBuffer is a wl_buffer backed by client GPU memory.
279+type dmabufBuffer struct{ buf *ports.DMABuf }
280+
281+func (*dmabufBuffer) Destroy(*wayland.Buffer) {}
282+
283+func (b *dmabufBuffer) close() {
284+ for _, pl := range b.buf.Planes {
285+ pl.File.Close()
286+ }
287+}
288+
289+func (b *dmabufBuffer) size() (int, int) { return b.buf.Width, b.buf.Height }
290+
291+// content hands the renderer the buffer itself: nothing is copied.
292+func (b *dmabufBuffer) content(id ports.WindowID) (ports.SurfaceContent, bool) {
293+ opaque := b.buf.Format == fourccXRGB || b.buf.Format == fourccXBGR
294+ return ports.SurfaceContent{ID: id, Width: b.buf.Width, Height: b.buf.Height, Opaque: opaque, DMABuf: b.buf}, true
295+}
296+
297+const (
298+ fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
299+ fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
300+)
A internal/adapters/wayland/dmabuf_test.go +174 -0
@@ -0,0 +1,174 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os/exec"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/nefertty/internal/logging"
10+ "github.com/bnema/nefertty/internal/ports"
11+ "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
12+ "github.com/bnema/wlturbo"
13+ "golang.org/x/sys/unix"
14+)
15+
16+var linearARGB = ports.DMABufFormat{Format: 'A' | 'R'<<8 | '2'<<16 | '4'<<24}
17+
18+func dmabufServer(t *testing.T, sup ports.DMABufSupport) (*Server, chan ports.ClientEvent, chan ports.SurfaceContent, string) {
19+ t.Helper()
20+ dir := t.TempDir()
21+ events := make(chan ports.ClientEvent, 16)
22+ contents := make(chan ports.SurfaceContent, 16)
23+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland"))
24+ if err != nil {
25+ t.Fatal(err)
26+ }
27+ ctx, cancel := context.WithCancel(context.Background())
28+ done := make(chan error, 1)
29+ go func() { done <- s.Run(ctx) }()
30+ t.Cleanup(func() {
31+ cancel()
32+ select {
33+ case err := <-done:
34+ if err != nil {
35+ t.Errorf("Run: %v", err)
36+ }
37+ case <-time.After(2 * time.Second):
38+ t.Error("Run hung")
39+ }
40+ })
41+ return s, events, contents, dir
42+}
43+
44+// Without renderer formats there is no linux-dmabuf global.
45+func TestNoDMABufWithoutFormats(t *testing.T) {
46+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
47+ c := protocolClient(t, s, dir)
48+ if _, ok := c.Registry().FindGlobal("zwp_linux_dmabuf_v1"); ok {
49+ t.Fatal("zwp_linux_dmabuf_v1 advertised")
50+ }
51+}
52+
53+type paramsProxy struct {
54+ wlturbo.BaseProxy
55+ result chan uint16
56+}
57+
58+func (p *paramsProxy) Dispatch(e *wlturbo.Event) { p.result <- e.Opcode }
59+
60+// A format the renderer did not list fails; a plane past the end of its
61+// file is a protocol error.
62+func TestDMABufParams(t *testing.T) {
63+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
64+ c := protocolClient(t, s, dir)
65+ g, ok := c.Registry().FindGlobal("zwp_linux_dmabuf_v1")
66+ if !ok || g.Version != dmabufVersion {
67+ t.Fatalf("global %+v", g)
68+ }
69+ dm, err := c.Registry().BindID(g.Name, g.Interface, dmabufVersion)
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
74+ if err != nil {
75+ t.Fatal(err)
76+ }
77+ defer unix.Close(fd)
78+ if err := unix.Ftruncate(fd, 256*16); err != nil {
79+ t.Fatal(err)
80+ }
81+ create := func(format uint32, height int32) (uint32, chan uint16) {
82+ p := c.AllocateID()
83+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
84+ proxy := &paramsProxy{result: make(chan uint16, 2)}
85+ proxy.SetID(p)
86+ c.Context().Register(proxy)
87+ if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
88+ t.Fatal(err)
89+ }
90+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), height, format, uint32(0))
91+ if err := c.Roundtrip(); err != nil {
92+ t.Fatal(err)
93+ }
94+ return p, proxy.result
95+ }
96+ // NV12 is not in the list: failed, not a protocol error.
97+ _, result := create('N'|'V'<<8|'1'<<16|'2'<<24, 16)
98+ if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
99+ t.Fatalf("event %d", op)
100+ }
101+ // Supported: created (import errors surface in the renderer).
102+ _, result = create(linearARGB.Format, 16)
103+ if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
104+ t.Fatalf("event %d", op)
105+ }
106+ // Taller than the file: out of bounds.
107+ big := c.AllocateID()
108+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, big)
109+ if err := c.SendRequestWithFDs(big, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
110+ t.Fatal(err)
111+ }
112+ requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0))
113+ expectProtocolError(t, c, big, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds))
114+}
115+
116+// A reused params object is a protocol error.
117+func TestDMABufParamsReuse(t *testing.T) {
118+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
119+ c := protocolClient(t, s, dir)
120+ g, _ := c.Registry().FindGlobal("zwp_linux_dmabuf_v1")
121+ dm, err := c.Registry().BindID(g.Name, g.Interface, dmabufVersion)
122+ if err != nil {
123+ t.Fatal(err)
124+ }
125+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
126+ if err != nil {
127+ t.Fatal(err)
128+ }
129+ defer unix.Close(fd)
130+ if err := unix.Ftruncate(fd, 256*16); err != nil {
131+ t.Fatal(err)
132+ }
133+ p := c.AllocateID()
134+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
135+ proxy := &paramsProxy{result: make(chan uint16, 2)}
136+ proxy.SetID(p)
137+ c.Context().Register(proxy)
138+ if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
139+ t.Fatal(err)
140+ }
141+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
142+ if err := c.Roundtrip(); err != nil {
143+ t.Fatal(err)
144+ }
145+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
146+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed))
147+}
148+
149+// weston-simple-dmabuf-egl renders on the GPU and sends dmabufs: its
150+// contents reach the renderer as DMABuf, never as pixels.
151+func TestWestonDMABuf(t *testing.T) {
152+ tool, err := exec.LookPath("weston-simple-dmabuf-egl")
153+ if err != nil {
154+ t.Skip("weston-simple-dmabuf-egl not installed")
155+ }
156+ // Linear buffers: every GPU allocates them, and the client picks from
157+ // what it is offered.
158+ xrgb := ports.DMABufFormat{Format: fourccXRGB}
159+ s, events, contents, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB, xrgb}})
160+ _, _, output := lifecycleClient(t, tool, s, dir, nil)
161+ w := mapped(t, events, 5*time.Second)
162+ deadline := time.After(5 * time.Second)
163+ for {
164+ select {
165+ case c := <-contents:
166+ if c.ID != w.ID || c.DMABuf == nil || c.Pixels != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {
167+ t.Fatalf("content %+v", c)
168+ }
169+ return
170+ case <-deadline:
171+ t.Fatalf("no dmabuf content: %s", output.String())
172+ }
173+ }
174+}
@@ -0,0 +1,174 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "os/exec"
6+ "testing"
7+ "time"
8+
9+ "github.com/bnema/nefertty/internal/logging"
10+ "github.com/bnema/nefertty/internal/ports"
11+ "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
12+ "github.com/bnema/wlturbo"
13+ "golang.org/x/sys/unix"
14+)
15+
16+var linearARGB = ports.DMABufFormat{Format: 'A' | 'R'<<8 | '2'<<16 | '4'<<24}
17+
18+func dmabufServer(t *testing.T, sup ports.DMABufSupport) (*Server, chan ports.ClientEvent, chan ports.SurfaceContent, string) {
19+ t.Helper()
20+ dir := t.TempDir()
21+ events := make(chan ports.ClientEvent, 16)
22+ contents := make(chan ports.SurfaceContent, 16)
23+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland"))
24+ if err != nil {
25+ t.Fatal(err)
26+ }
27+ ctx, cancel := context.WithCancel(context.Background())
28+ done := make(chan error, 1)
29+ go func() { done <- s.Run(ctx) }()
30+ t.Cleanup(func() {
31+ cancel()
32+ select {
33+ case err := <-done:
34+ if err != nil {
35+ t.Errorf("Run: %v", err)
36+ }
37+ case <-time.After(2 * time.Second):
38+ t.Error("Run hung")
39+ }
40+ })
41+ return s, events, contents, dir
42+}
43+
44+// Without renderer formats there is no linux-dmabuf global.
45+func TestNoDMABufWithoutFormats(t *testing.T) {
46+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
47+ c := protocolClient(t, s, dir)
48+ if _, ok := c.Registry().FindGlobal("zwp_linux_dmabuf_v1"); ok {
49+ t.Fatal("zwp_linux_dmabuf_v1 advertised")
50+ }
51+}
52+
53+type paramsProxy struct {
54+ wlturbo.BaseProxy
55+ result chan uint16
56+}
57+
58+func (p *paramsProxy) Dispatch(e *wlturbo.Event) { p.result <- e.Opcode }
59+
60+// A format the renderer did not list fails; a plane past the end of its
61+// file is a protocol error.
62+func TestDMABufParams(t *testing.T) {
63+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
64+ c := protocolClient(t, s, dir)
65+ g, ok := c.Registry().FindGlobal("zwp_linux_dmabuf_v1")
66+ if !ok || g.Version != dmabufVersion {
67+ t.Fatalf("global %+v", g)
68+ }
69+ dm, err := c.Registry().BindID(g.Name, g.Interface, dmabufVersion)
70+ if err != nil {
71+ t.Fatal(err)
72+ }
73+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
74+ if err != nil {
75+ t.Fatal(err)
76+ }
77+ defer unix.Close(fd)
78+ if err := unix.Ftruncate(fd, 256*16); err != nil {
79+ t.Fatal(err)
80+ }
81+ create := func(format uint32, height int32) (uint32, chan uint16) {
82+ p := c.AllocateID()
83+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
84+ proxy := &paramsProxy{result: make(chan uint16, 2)}
85+ proxy.SetID(p)
86+ c.Context().Register(proxy)
87+ if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
88+ t.Fatal(err)
89+ }
90+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), height, format, uint32(0))
91+ if err := c.Roundtrip(); err != nil {
92+ t.Fatal(err)
93+ }
94+ return p, proxy.result
95+ }
96+ // NV12 is not in the list: failed, not a protocol error.
97+ _, result := create('N'|'V'<<8|'1'<<16|'2'<<24, 16)
98+ if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
99+ t.Fatalf("event %d", op)
100+ }
101+ // Supported: created (import errors surface in the renderer).
102+ _, result = create(linearARGB.Format, 16)
103+ if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
104+ t.Fatalf("event %d", op)
105+ }
106+ // Taller than the file: out of bounds.
107+ big := c.AllocateID()
108+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, big)
109+ if err := c.SendRequestWithFDs(big, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
110+ t.Fatal(err)
111+ }
112+ requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0))
113+ expectProtocolError(t, c, big, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds))
114+}
115+
116+// A reused params object is a protocol error.
117+func TestDMABufParamsReuse(t *testing.T) {
118+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
119+ c := protocolClient(t, s, dir)
120+ g, _ := c.Registry().FindGlobal("zwp_linux_dmabuf_v1")
121+ dm, err := c.Registry().BindID(g.Name, g.Interface, dmabufVersion)
122+ if err != nil {
123+ t.Fatal(err)
124+ }
125+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
126+ if err != nil {
127+ t.Fatal(err)
128+ }
129+ defer unix.Close(fd)
130+ if err := unix.Ftruncate(fd, 256*16); err != nil {
131+ t.Fatal(err)
132+ }
133+ p := c.AllocateID()
134+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
135+ proxy := &paramsProxy{result: make(chan uint16, 2)}
136+ proxy.SetID(p)
137+ c.Context().Register(proxy)
138+ if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
139+ t.Fatal(err)
140+ }
141+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
142+ if err := c.Roundtrip(); err != nil {
143+ t.Fatal(err)
144+ }
145+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
146+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed))
147+}
148+
149+// weston-simple-dmabuf-egl renders on the GPU and sends dmabufs: its
150+// contents reach the renderer as DMABuf, never as pixels.
151+func TestWestonDMABuf(t *testing.T) {
152+ tool, err := exec.LookPath("weston-simple-dmabuf-egl")
153+ if err != nil {
154+ t.Skip("weston-simple-dmabuf-egl not installed")
155+ }
156+ // Linear buffers: every GPU allocates them, and the client picks from
157+ // what it is offered.
158+ xrgb := ports.DMABufFormat{Format: fourccXRGB}
159+ s, events, contents, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB, xrgb}})
160+ _, _, output := lifecycleClient(t, tool, s, dir, nil)
161+ w := mapped(t, events, 5*time.Second)
162+ deadline := time.After(5 * time.Second)
163+ for {
164+ select {
165+ case c := <-contents:
166+ if c.ID != w.ID || c.DMABuf == nil || c.Pixels != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {
167+ t.Fatalf("content %+v", c)
168+ }
169+ return
170+ case <-deadline:
171+ t.Fatalf("no dmabuf content: %s", output.String())
172+ }
173+ }
174+}
M internal/adapters/wayland/globals.go +20 -0
@@ -15,6 +15,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
1515 func() error { return registerXDGOutput(d, s) },
1616 func() error { return registerDecoration(d, s) },
1717 func() error { return registerScale(d, s) },
18+ func() error { return registerDMABuf(d, s, o.DMABuf) },
1819 func() error {
1920 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
2021 },
@@ -204,6 +205,25 @@ type buffer struct {
204205
205206 func (*buffer) Destroy(*wayland.Buffer) {}
206207
208+func (b *buffer) size() (int, int) { return b.width, b.height }
209+
210+// clientBuffer is a wl_buffer's content source: wl_shm or linux-dmabuf.
211+type clientBuffer interface {
212+ // content returns what the renderer draws; false means the client
213+ // broke its buffer (a truncated shm file).
214+ content(ports.WindowID) (ports.SurfaceContent, bool)
215+ size() (int, int)
216+}
217+
218+// addBuffer tracks a buffer until the client destroys it.
219+func (s *Server) addBuffer(r *wayland.Buffer, b *dmabufBuffer) {
220+ s.buffers[r.Resource] = b
221+ r.OnDestroy = func() {
222+ delete(s.buffers, r.Resource)
223+ b.close()
224+ }
225+}
226+
207227 // A malicious client may truncate its fd after mmap; SetPanicOnFault converts
208228 // the resulting SIGBUS on this goroutine to a recoverable panic.
209229 func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bool) {
@@ -15,6 +15,7 @@ func registerGlobals(d *server.Display, o Options, s *Server) error {
15 func() error { return registerXDGOutput(d, s) },15 func() error { return registerXDGOutput(d, s) },
16 func() error { return registerDecoration(d, s) },16 func() error { return registerDecoration(d, s) },
17 func() error { return registerScale(d, s) },17 func() error { return registerScale(d, s) },
18+ func() error { return registerDMABuf(d, s, o.DMABuf) },
18 func() error {19 func() error {
19 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })20 return wayland.NewCompositorGlobal(d, 6, func(c server.Client, v, id uint32) { wayland.NewCompositor(c, int32(v), id, compositor{s}) })
20 },21 },
@@ -204,6 +205,25 @@ type buffer struct {
204 205
205 func (*buffer) Destroy(*wayland.Buffer) {}206 func (*buffer) Destroy(*wayland.Buffer) {}
206 207
208+func (b *buffer) size() (int, int) { return b.width, b.height }
209+
210+// clientBuffer is a wl_buffer's content source: wl_shm or linux-dmabuf.
211+type clientBuffer interface {
212+ // content returns what the renderer draws; false means the client
213+ // broke its buffer (a truncated shm file).
214+ content(ports.WindowID) (ports.SurfaceContent, bool)
215+ size() (int, int)
216+}
217+
218+// addBuffer tracks a buffer until the client destroys it.
219+func (s *Server) addBuffer(r *wayland.Buffer, b *dmabufBuffer) {
220+ s.buffers[r.Resource] = b
221+ r.OnDestroy = func() {
222+ delete(s.buffers, r.Resource)
223+ b.close()
224+ }
225+}
226+
207 // A malicious client may truncate its fd after mmap; SetPanicOnFault converts227 // A malicious client may truncate its fd after mmap; SetPanicOnFault converts
208 // the resulting SIGBUS on this goroutine to a recoverable panic.228 // the resulting SIGBUS on this goroutine to a recoverable panic.
209 func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bool) {229 func (b *buffer) content(id ports.WindowID) (content ports.SurfaceContent, ok bool) {
M internal/adapters/wayland/layer.go +1 -1
@@ -166,7 +166,7 @@ func (s *Server) layerChanged() {
166166 }
167167 if l.surface.current != nil {
168168 if b := s.buffers[l.surface.current.Resource]; b != nil {
169- v.Width, v.Height = l.surface.logicalSize(b.width, b.height)
169+ v.Width, v.Height = l.surface.logicalSize(b.size())
170170 }
171171 }
172172 list = append(list, v)
@@ -166,7 +166,7 @@ func (s *Server) layerChanged() {
166 }166 }
167 if l.surface.current != nil {167 if l.surface.current != nil {
168 if b := s.buffers[l.surface.current.Resource]; b != nil {168 if b := s.buffers[l.surface.current.Resource]; b != nil {
169- v.Width, v.Height = l.surface.logicalSize(b.width, b.height)169+ v.Width, v.Height = l.surface.logicalSize(b.size())
170 }170 }
171 }171 }
172 list = append(list, v)172 list = append(list, v)
M internal/adapters/wayland/server.go +8 -4
@@ -19,8 +19,10 @@ import (
1919 // Options configures the server. Outputs is the initial layout; core
2020 // replaces it with ports.SetOutputs.
2121 type Options struct {
22- RuntimeDir string
23- Outputs ports.Layout
22+ RuntimeDir string
23+ Outputs ports.Layout
24+ // DMABuf is what the renderer imports; empty disables linux-dmabuf.
25+ DMABuf ports.DMABufSupport
2426 Keymap string
2527 RepeatRate, RepeatDelay int
2628 }
@@ -43,7 +45,8 @@ type Server struct {
4345 frames uint64
4446 started time.Time
4547 surfaces map[*server.Resource]*surface
46- buffers map[*server.Resource]*buffer
48+ buffers map[*server.Resource]clientBuffer
49+ dmabuf *dmabufGlobal
4750 serial uint32
4851 ctx context.Context
4952 windows map[ports.WindowID]*window
@@ -108,7 +111,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
108111 d.Close()
109112 return nil, err
110113 }
111- s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
114+ s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
112115 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
113116 if opts.Keymap != "" {
114117 fd, size, e := keymapFile(opts.Keymap)
@@ -123,6 +126,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
123126 if s.keymapFD >= 0 {
124127 unix.Close(s.keymapFD)
125128 }
129+ s.dmabuf.close()
126130 cleanup()
127131 }
128132 if err = registerGlobals(d, opts, s); err != nil {
@@ -19,8 +19,10 @@ import (
19 // Options configures the server. Outputs is the initial layout; core19 // Options configures the server. Outputs is the initial layout; core
20 // replaces it with ports.SetOutputs.20 // replaces it with ports.SetOutputs.
21 type Options struct {21 type Options struct {
22- RuntimeDir string22+ RuntimeDir string
23- Outputs ports.Layout23+ Outputs ports.Layout
24+ // DMABuf is what the renderer imports; empty disables linux-dmabuf.
25+ DMABuf ports.DMABufSupport
24 Keymap string26 Keymap string
25 RepeatRate, RepeatDelay int27 RepeatRate, RepeatDelay int
26 }28 }
@@ -43,7 +45,8 @@ type Server struct {
43 frames uint6445 frames uint64
44 started time.Time46 started time.Time
45 surfaces map[*server.Resource]*surface47 surfaces map[*server.Resource]*surface
46- buffers map[*server.Resource]*buffer48+ buffers map[*server.Resource]clientBuffer
49+ dmabuf *dmabufGlobal
47 serial uint3250 serial uint32
48 ctx context.Context51 ctx context.Context
49 windows map[ports.WindowID]*window52 windows map[ports.WindowID]*window
@@ -108,7 +111,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
108 d.Close()111 d.Close()
109 return nil, err112 return nil, err
110 }113 }
111- s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]*buffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}114+ s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
112 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}115 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
113 if opts.Keymap != "" {116 if opts.Keymap != "" {
114 fd, size, e := keymapFile(opts.Keymap)117 fd, size, e := keymapFile(opts.Keymap)
@@ -123,6 +126,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
123 if s.keymapFD >= 0 {126 if s.keymapFD >= 0 {
124 unix.Close(s.keymapFD)127 unix.Close(s.keymapFD)
125 }128 }
129+ s.dmabuf.close()
126 cleanup()130 cleanup()
127 }131 }
128 if err = registerGlobals(d, opts, s); err != nil {132 if err = registerGlobals(d, opts, s); err != nil {
M internal/adapters/wayland/surface.go +9 -5
@@ -107,13 +107,17 @@ func (s *surface) Commit(*wayland.Surface) {
107107 s.server.log.Info().Uint64("id", uint64(id)).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
108108 }
109109 s.server.emitContent(c)
110- } else {
111- state.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
110+ } else if shm, ok := state.(*buffer); ok {
111+ shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
112112 }
113- if b.Resource.Alive() {
114- b.SendRelease()
113+ // wl_shm pixels were copied: the client may reuse the buffer.
114+ // A dmabuf is read in place until the next buffer replaces it.
115+ if _, gpu := state.(*dmabufBuffer); !gpu {
116+ if b.Resource.Alive() {
117+ b.SendRelease()
118+ }
119+ s.released = true
115120 }
116- s.released = true
117121 }
118122 }
119123 }
@@ -107,13 +107,17 @@ func (s *surface) Commit(*wayland.Surface) {
107 s.server.log.Info().Uint64("id", uint64(id)).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")107 s.server.log.Info().Uint64("id", uint64(id)).Int("w", c.Width).Int("h", c.Height).Msg("buffer size")
108 }108 }
109 s.server.emitContent(c)109 s.server.emitContent(c)
110- } else {110+ } else if shm, ok := state.(*buffer); ok {
111- state.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")111+ shm.pool.shm.PostError(uint32(wayland.ShmErrorInvalidFd), "SHM backing file truncated")
112 }112 }
113- if b.Resource.Alive() {113+ // wl_shm pixels were copied: the client may reuse the buffer.
114- b.SendRelease()114+ // A dmabuf is read in place until the next buffer replaces it.
115+ if _, gpu := state.(*dmabufBuffer); !gpu {
116+ if b.Resource.Alive() {
117+ b.SendRelease()
118+ }
119+ s.released = true
115 }120 }
116- s.released = true
117 }121 }
118 }122 }
119 }123 }
M internal/app/app.go +4 -1
@@ -89,7 +89,10 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
8989 }
9090 keymap := km.String()
9191 runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
92- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents}, logging.For(ctx, "wayland"))
92+ // Every output renders on the same GPU: its formats are the clients'.
93+ dmabuf := vulkan.Probe()
94+ log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
95+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents}, logging.For(ctx, "wayland"))
9396 if err != nil {
9497 km.Close()
9598 return err
@@ -89,7 +89,10 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
89 }89 }
90 keymap := km.String()90 keymap := km.String()
91 runtimeDir := os.Getenv("XDG_RUNTIME_DIR")91 runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
92- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents}, logging.For(ctx, "wayland"))92+ // Every output renders on the same GPU: its formats are the clients'.
93+ dmabuf := vulkan.Probe()
94+ log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
95+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents}, logging.For(ctx, "wayland"))
93 if err != nil {96 if err != nil {
94 km.Close()97 km.Close()
95 return err98 return err
M internal/app/outputs.go +1 -1
@@ -77,7 +77,7 @@ func (s *outputSet) scenes(set []ports.Scene) {
7777 }
7878
7979 func (s *outputSet) content(c ports.SurfaceContent) {
80- if c.Pixels == nil {
80+ if c.Empty() {
8181 delete(s.latest, c.ID)
8282 } else {
8383 s.latest[c.ID] = c
@@ -77,7 +77,7 @@ func (s *outputSet) scenes(set []ports.Scene) {
77 }77 }
78 78
79 func (s *outputSet) content(c ports.SurfaceContent) {79 func (s *outputSet) content(c ports.SurfaceContent) {
80- if c.Pixels == nil {80+ if c.Empty() {
81 delete(s.latest, c.ID)81 delete(s.latest, c.ID)
82 } else {82 } else {
83 s.latest[c.ID] = c83 s.latest[c.ID] = c
M internal/ports/messages.go +45 -6
@@ -1,5 +1,7 @@
11 package ports
22
3+import "os"
4+
35 // ClientEvent carries wayland → core notifications.
46 type ClientEvent interface{ clientEvent() }
57
@@ -338,18 +340,55 @@ type SceneLayer struct {
338340 Rect Rect
339341 }
340342
341-// SurfaceContent carries wayland → output the latest committed pixels of a
342-// window. Pixels is a private copy in B8G8R8A8 (wl_shm argb8888/xrgb8888
343-// little-endian) with Stride bytes per row; receivers never modify it.
344-// Pixels == nil means the window has no content. LogicalW and LogicalH are
345-// the surface size in logical pixels (buffer scale and viewport applied).
343+// SurfaceContent carries wayland → output the latest committed content of a
344+// window: either Pixels, a private copy in B8G8R8A8 (wl_shm
345+// argb8888/xrgb8888 little-endian) with Stride bytes per row, or DMABuf, a
346+// GPU buffer the renderer reads in place. Receivers never modify either.
347+// Empty means the window has no content. LogicalW and LogicalH are the
348+// surface size in logical pixels (buffer scale and viewport applied).
346349 type SurfaceContent struct {
347350 ID WindowID
348351 Width, Height int
349352 LogicalW, LogicalH int
350353 Stride int
351- Opaque bool // xrgb8888: ignore the alpha byte
354+ Opaque bool // x formats: ignore the alpha byte
352355 Pixels []byte
356+ DMABuf *DMABuf
357+}
358+
359+// Empty reports whether the content has nothing to draw.
360+func (c SurfaceContent) Empty() bool { return c.Pixels == nil && c.DMABuf == nil }
361+
362+// DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while
363+// the client's buffer exists; a renderer that keeps it must duplicate the
364+// descriptors under File.SyscallConn so a concurrent close cannot hand it a
365+// reused one. ID is unique per buffer for the whole session: renderers key
366+// their imports on it.
367+type DMABuf struct {
368+ ID uint64
369+ Width, Height int
370+ Format uint32 // DRM fourcc
371+ Modifier uint64
372+ Planes []DMABufPlane
373+}
374+
375+// DMABufPlane is one plane of a DMABuf.
376+type DMABufPlane struct {
377+ File *os.File
378+ Offset, Stride uint32
379+}
380+
381+// DMABufFormat is a DRM fourcc with a modifier the renderer can import.
382+type DMABufFormat struct {
383+ Format uint32
384+ Modifier uint64
385+}
386+
387+// DMABufSupport is what the renderer imports: the render device (a dev_t,
388+// 0 when unknown) and the formats. No formats means no dmabuf.
389+type DMABufSupport struct {
390+ Device uint64
391+ Formats []DMABufFormat
353392 }
354393
355394 // SceneWindow carries core → renderer window placement.
@@ -1,5 +1,7 @@
1 package ports1 package ports
2 2
3+import "os"
4+
3 // ClientEvent carries wayland → core notifications.5 // ClientEvent carries wayland → core notifications.
4 type ClientEvent interface{ clientEvent() }6 type ClientEvent interface{ clientEvent() }
5 7
@@ -338,18 +340,55 @@ type SceneLayer struct {
338 Rect Rect340 Rect Rect
339 }341 }
340 342
341-// SurfaceContent carries wayland → output the latest committed pixels of a343+// SurfaceContent carries wayland → output the latest committed content of a
342-// window. Pixels is a private copy in B8G8R8A8 (wl_shm argb8888/xrgb8888344+// window: either Pixels, a private copy in B8G8R8A8 (wl_shm
343-// little-endian) with Stride bytes per row; receivers never modify it.345+// argb8888/xrgb8888 little-endian) with Stride bytes per row, or DMABuf, a
344-// Pixels == nil means the window has no content. LogicalW and LogicalH are346+// GPU buffer the renderer reads in place. Receivers never modify either.
345-// the surface size in logical pixels (buffer scale and viewport applied).347+// Empty means the window has no content. LogicalW and LogicalH are the
348+// surface size in logical pixels (buffer scale and viewport applied).
346 type SurfaceContent struct {349 type SurfaceContent struct {
347 ID WindowID350 ID WindowID
348 Width, Height int351 Width, Height int
349 LogicalW, LogicalH int352 LogicalW, LogicalH int
350 Stride int353 Stride int
351- Opaque bool // xrgb8888: ignore the alpha byte354+ Opaque bool // x formats: ignore the alpha byte
352 Pixels []byte355 Pixels []byte
356+ DMABuf *DMABuf
357+}
358+
359+// Empty reports whether the content has nothing to draw.
360+func (c SurfaceContent) Empty() bool { return c.Pixels == nil && c.DMABuf == nil }
361+
362+// DMABuf is a client GPU buffer (linux-dmabuf). Its files stay open while
363+// the client's buffer exists; a renderer that keeps it must duplicate the
364+// descriptors under File.SyscallConn so a concurrent close cannot hand it a
365+// reused one. ID is unique per buffer for the whole session: renderers key
366+// their imports on it.
367+type DMABuf struct {
368+ ID uint64
369+ Width, Height int
370+ Format uint32 // DRM fourcc
371+ Modifier uint64
372+ Planes []DMABufPlane
373+}
374+
375+// DMABufPlane is one plane of a DMABuf.
376+type DMABufPlane struct {
377+ File *os.File
378+ Offset, Stride uint32
379+}
380+
381+// DMABufFormat is a DRM fourcc with a modifier the renderer can import.
382+type DMABufFormat struct {
383+ Format uint32
384+ Modifier uint64
385+}
386+
387+// DMABufSupport is what the renderer imports: the render device (a dev_t,
388+// 0 when unknown) and the formats. No formats means no dmabuf.
389+type DMABufSupport struct {
390+ Device uint64
391+ Formats []DMABufFormat
353 }392 }
354 393
355 // SceneWindow carries core → renderer window placement.394 // SceneWindow carries core → renderer window placement.

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 26

Tags 10

↑↓ moveenter openesc close