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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 178 | 178 | } | |
| 179 | 179 | scene, haveScene, dirty = s, true, true | |
| 180 | 180 | case c := <-contents: | |
| 181 | - | if c.Pixels == nil { | |
| 181 | + | if c.Empty() { | |
| 182 | 182 | delete(surfaces, c.ID) | |
| 183 | 183 | } else { | |
| 184 | 184 | 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, true | 179 | 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] = c | 184 | 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 | |||
| 36 | 36 | frame := 0 | |
| 37 | 37 | cursorScale := -1.0 // not loaded yet | |
| 38 | 38 | update := func(c ports.SurfaceContent) { | |
| 39 | - | if c.Pixels == nil { | |
| 39 | + | if c.Empty() { | |
| 40 | 40 | delete(surfaces, c.ID) | |
| 41 | 41 | } else { | |
| 42 | 42 | surfaces[c.ID] = c | |
| @@ -36,7 +36,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents | |||||
| 36 | frame := 0 | 36 | frame := 0 | ||
| 37 | cursorScale := -1.0 // not loaded yet | 37 | 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] = c | 42 | 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 ( | |||
| 6 | 6 | "math" | |
| 7 | 7 | "os" | |
| 8 | 8 | "runtime" | |
| 9 | + | "slices" | |
| 9 | 10 | "strconv" | |
| 10 | 11 | "unsafe" | |
| 11 | 12 | ||
| @@ -36,6 +37,11 @@ type Renderer struct { | |||
| 36 | 37 | fence vk.Fence | |
| 37 | 38 | layout vk.ImageLayout | |
| 38 | 39 | 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 | 47 | func checked(name string, result vk.Result) error { | |
| @@ -49,7 +55,7 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 49 | 55 | if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) { | |
| 50 | 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 | 59 | defer func() { | |
| 54 | 60 | if err != nil { | |
| 55 | 61 | r.Close() | |
| @@ -123,10 +129,22 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 123 | 129 | return | |
| 124 | 130 | } | |
| 125 | 131 | r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory) | |
| 132 | + | r.family = family | |
| 126 | 133 | priority := float32(1) | |
| 127 | 134 | qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority} | |
| 128 | 135 | dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1} | |
| 129 | 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 | 148 | if err = checked("vkCreateDevice", r.id.CreateDevice(physical, &di, nil, &r.device)); err != nil { | |
| 131 | 149 | return | |
| 132 | 150 | } | |
| @@ -135,6 +153,14 @@ func New(width, height int) (r *Renderer, err error) { | |||
| 135 | 153 | err = fmt.Errorf("LoadDeviceDispatch: %w", err) | |
| 136 | 154 | 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 | 164 | r.dd.GetDeviceQueue(r.device, family, 0, &r.queue) | |
| 139 | 165 | extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1} | |
| 140 | 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 | 358 | dst, src image.Rectangle | |
| 333 | 359 | color [3]uint8 | |
| 334 | 360 | opaque bool | |
| 361 | + | // dma is a client GPU buffer, blitted instead of copied from staging. | |
| 362 | + | dma *imported | |
| 335 | 363 | } | |
| 336 | 364 | var uploads []upload | |
| 337 | 365 | used := 0 | |
| @@ -350,6 +378,15 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 350 | 378 | if rect.Empty() || src.Empty() { | |
| 351 | 379 | 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 | 390 | uploads = append(uploads, upload{rect: rect, offset: used, pixels: content.Pixels, stride: content.Stride, opaque: content.Opaque, dst: dst, src: src}) | |
| 354 | 391 | 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 | 396 | if lw <= 0 || lh <= 0 { | |
| 360 | 397 | 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 | 403 | return | |
| 364 | 404 | } | |
| 365 | 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 | 456 | cx, cy, cw, ch := x+b, y+b, w.Rect.W-2*b, w.Rect.H-2*b | |
| 417 | 457 | body := physRect(cx, cy, cw, ch) | |
| 418 | 458 | content := contents[w.ID] | |
| 419 | - | if content.Pixels == nil { | |
| 459 | + | if content.Empty() { | |
| 420 | 460 | add(body, windowColor(w.ID)) | |
| 421 | 461 | } else { | |
| 422 | 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 | 486 | data := unsafe.Slice((*byte)(r.stagingMapped), r.stagingSize) | |
| 447 | 487 | for _, u := range uploads { | |
| 488 | + | if u.dma != nil { | |
| 489 | + | continue | |
| 490 | + | } | |
| 448 | 491 | dst := data[u.offset : u.offset+u.rect.Dx()*u.rect.Dy()*4] | |
| 449 | 492 | for row := 0; row < u.rect.Dy(); row++ { | |
| 450 | 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 | 545 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| 503 | 546 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | |
| 504 | 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 | 573 | for i, f := range uploads { | |
| 506 | 574 | if i > 0 { | |
| 507 | 575 | d.CmdPipelineBarrier(r.command, vk.PipelineStageTransferBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &barrier) | |
| 508 | 576 | } | |
| 509 | 577 | 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 | 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 | 593 | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | |
| 512 | 594 | } | |
| 595 | + | ownership(false) | |
| 513 | 596 | barrier.DstAccessMask = vk.AccessTransferReadBit | |
| 514 | 597 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | |
| 515 | 598 | barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal | |
| @@ -519,14 +602,35 @@ func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.Surfa | |||
| 519 | 602 | if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(r.command)); err != nil { | |
| 520 | 603 | 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 | 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 | 625 | if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, r.fence)); err != nil { | |
| 524 | 626 | return err | |
| 525 | 627 | } | |
| 628 | + | runtime.KeepAlive(stages) | |
| 526 | 629 | r.layout = vk.ImageLayoutTransferSrcOptimal | |
| 527 | 630 | if err := checked("vkWaitForFences", d.WaitForFences(r.device, 1, &r.fence, 1, math.MaxUint64)); err != nil { | |
| 528 | 631 | return err | |
| 529 | 632 | } | |
| 633 | + | r.dropUnused() | |
| 530 | 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 | 642 | src := unsafe.Slice((*byte)(r.mapped), len(out.Pix)) | |
| 539 | 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 | 647 | return out | |
| 543 | 648 | } | |
| @@ -564,6 +669,10 @@ func (r *Renderer) Close() { | |||
| 564 | 669 | if r.device != 0 { | |
| 565 | 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 | 676 | if r.fence != 0 { | |
| 568 | 677 | d.DestroyFence(r.device, r.fence, nil) | |
| 569 | 678 | 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.Fence | 37 | fence vk.Fence | ||
| 37 | layout vk.ImageLayout | 38 | layout vk.ImageLayout | ||
| 38 | memory vk.PhysicalDeviceMemoryProperties | 39 | 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 | return | 129 | 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 | return | 149 | 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 | return | 154 | 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.Rectangle | 358 | dst, src image.Rectangle | ||
| 333 | color [3]uint8 | 359 | color [3]uint8 | ||
| 334 | opaque bool | 360 | opaque bool | ||
| 361 | + | // dma is a client GPU buffer, blitted instead of copied from staging. | |||
| 362 | + | dma *imported | |||
| 335 | } | 363 | } | ||
| 336 | var uploads []upload | 364 | var uploads []upload | ||
| 337 | used := 0 | 365 | 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 | return | 379 | 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() * 4 | 391 | 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.Height | 397 | 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 | return | 403 | 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*b | 456 | 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.ImageLayoutTransferDstOptimal | 545 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | ||
| 503 | barrier.NewLayout = vk.ImageLayoutTransferDstOptimal | 546 | 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.rect | 577 | 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, ®ion) | 593 | d.CmdCopyBufferToImage(r.command, r.staging, r.image, vk.ImageLayoutTransferDstOptimal, 1, ®ion) | ||
| 512 | } | 594 | } | ||
| 595 | + | ownership(false) | |||
| 513 | barrier.DstAccessMask = vk.AccessTransferReadBit | 596 | barrier.DstAccessMask = vk.AccessTransferReadBit | ||
| 514 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | 597 | barrier.OldLayout = vk.ImageLayoutTransferDstOptimal | ||
| 515 | barrier.NewLayout = vk.ImageLayoutTransferSrcOptimal | 598 | 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 err | 603 | 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 err | 626 | return err | ||
| 525 | } | 627 | } | ||
| 628 | + | runtime.KeepAlive(stages) | |||
| 526 | r.layout = vk.ImageLayoutTransferSrcOptimal | 629 | 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 err | 631 | 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 out | 647 | 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 = 0 | 678 | 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 := ¶ms{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 := ¶ms{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 := ¶msProxy{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 := ¶msProxy{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 := ¶msProxy{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 := ¶msProxy{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 { | |||
| 15 | 15 | func() error { return registerXDGOutput(d, s) }, | |
| 16 | 16 | func() error { return registerDecoration(d, s) }, | |
| 17 | 17 | func() error { return registerScale(d, s) }, | |
| 18 | + | func() error { return registerDMABuf(d, s, o.DMABuf) }, | |
| 18 | 19 | func() error { | |
| 19 | 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 | 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 | 227 | // A malicious client may truncate its fd after mmap; SetPanicOnFault converts | |
| 208 | 228 | // the resulting SIGBUS on this goroutine to a recoverable panic. | |
| 209 | 229 | 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 converts | 227 | // 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() { | |||
| 166 | 166 | } | |
| 167 | 167 | if l.surface.current != nil { | |
| 168 | 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 | 172 | 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 ( | |||
| 19 | 19 | // Options configures the server. Outputs is the initial layout; core | |
| 20 | 20 | // replaces it with ports.SetOutputs. | |
| 21 | 21 | 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 | |
| 24 | 26 | Keymap string | |
| 25 | 27 | RepeatRate, RepeatDelay int | |
| 26 | 28 | } | |
| @@ -43,7 +45,8 @@ type Server struct { | |||
| 43 | 45 | frames uint64 | |
| 44 | 46 | started time.Time | |
| 45 | 47 | surfaces map[*server.Resource]*surface | |
| 46 | - | buffers map[*server.Resource]*buffer | |
| 48 | + | buffers map[*server.Resource]clientBuffer | |
| 49 | + | dmabuf *dmabufGlobal | |
| 47 | 50 | serial uint32 | |
| 48 | 51 | ctx context.Context | |
| 49 | 52 | windows map[ports.WindowID]*window | |
| @@ -108,7 +111,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 108 | 111 | d.Close() | |
| 109 | 112 | 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 | 115 | s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{} | |
| 113 | 116 | if opts.Keymap != "" { | |
| 114 | 117 | fd, size, e := keymapFile(opts.Keymap) | |
| @@ -123,6 +126,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) { | |||
| 123 | 126 | if s.keymapFD >= 0 { | |
| 124 | 127 | unix.Close(s.keymapFD) | |
| 125 | 128 | } | |
| 129 | + | s.dmabuf.close() | |
| 126 | 130 | cleanup() | |
| 127 | 131 | } | |
| 128 | 132 | if err = registerGlobals(d, opts, s); err != nil { | |
| @@ -19,8 +19,10 @@ import ( | |||||
| 19 | // Options configures the server. Outputs is the initial layout; core | 19 | // 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 string | 22 | + | RuntimeDir string |
| 23 | - | Outputs ports.Layout | 23 | + | Outputs ports.Layout |
| 24 | + | // DMABuf is what the renderer imports; empty disables linux-dmabuf. | |||
| 25 | + | DMABuf ports.DMABufSupport | |||
| 24 | Keymap string | 26 | Keymap string | ||
| 25 | RepeatRate, RepeatDelay int | 27 | RepeatRate, RepeatDelay int | ||
| 26 | } | 28 | } | ||
| @@ -43,7 +45,8 @@ type Server struct { | |||||
| 43 | frames uint64 | 45 | frames uint64 | ||
| 44 | started time.Time | 46 | started time.Time | ||
| 45 | surfaces map[*server.Resource]*surface | 47 | surfaces map[*server.Resource]*surface | ||
| 46 | - | buffers map[*server.Resource]*buffer | 48 | + | buffers map[*server.Resource]clientBuffer |
| 49 | + | dmabuf *dmabufGlobal | |||
| 47 | serial uint32 | 50 | serial uint32 | ||
| 48 | ctx context.Context | 51 | ctx context.Context | ||
| 49 | windows map[ports.WindowID]*window | 52 | 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, err | 112 | 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) { | |||
| 107 | 107 | s.server.log.Info().Uint64("id", uint64(id)).Int("w", c.Width).Int("h", c.Height).Msg("buffer size") | |
| 108 | 108 | } | |
| 109 | 109 | 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") | |
| 112 | 112 | } | |
| 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 | |
| 115 | 120 | } | |
| 116 | - | s.released = true | |
| 117 | 121 | } | |
| 118 | 122 | } | |
| 119 | 123 | } | |
| @@ -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) | |||
| 89 | 89 | } | |
| 90 | 90 | keymap := km.String() | |
| 91 | 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 | 96 | if err != nil { | |
| 94 | 97 | km.Close() | |
| 95 | 98 | 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 err | 98 | return err | ||
M internal/app/outputs.go +1 -1
| @@ -77,7 +77,7 @@ func (s *outputSet) scenes(set []ports.Scene) { | |||
| 77 | 77 | } | |
| 78 | 78 | ||
| 79 | 79 | func (s *outputSet) content(c ports.SurfaceContent) { | |
| 80 | - | if c.Pixels == nil { | |
| 80 | + | if c.Empty() { | |
| 81 | 81 | delete(s.latest, c.ID) | |
| 82 | 82 | } else { | |
| 83 | 83 | 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] = c | 83 | s.latest[c.ID] = c | ||
M internal/ports/messages.go +45 -6
| @@ -1,5 +1,7 @@ | |||
| 1 | 1 | package ports | |
| 2 | 2 | ||
| 3 | + | import "os" | |
| 4 | + | ||
| 3 | 5 | // ClientEvent carries wayland → core notifications. | |
| 4 | 6 | type ClientEvent interface{ clientEvent() } | |
| 5 | 7 | ||
| @@ -338,18 +340,55 @@ type SceneLayer struct { | |||
| 338 | 340 | Rect Rect | |
| 339 | 341 | } | |
| 340 | 342 | ||
| 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). | |
| 346 | 349 | type SurfaceContent struct { | |
| 347 | 350 | ID WindowID | |
| 348 | 351 | Width, Height int | |
| 349 | 352 | LogicalW, LogicalH int | |
| 350 | 353 | Stride int | |
| 351 | - | Opaque bool // xrgb8888: ignore the alpha byte | |
| 354 | + | Opaque bool // x formats: ignore the alpha byte | |
| 352 | 355 | 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 | 394 | // SceneWindow carries core → renderer window placement. | |
| @@ -1,5 +1,7 @@ | |||||
| 1 | package ports | 1 | 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 Rect | 340 | Rect Rect | ||
| 339 | } | 341 | } | ||
| 340 | 342 | ||||
| 341 | - | // SurfaceContent carries wayland → output the latest committed pixels of a | 343 | + | // SurfaceContent carries wayland → output the latest committed content of a |
| 342 | - | // window. Pixels is a private copy in B8G8R8A8 (wl_shm argb8888/xrgb8888 | 344 | + | // 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 are | 346 | + | // 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 WindowID | 350 | ID WindowID | ||
| 348 | Width, Height int | 351 | Width, Height int | ||
| 349 | LogicalW, LogicalH int | 352 | LogicalW, LogicalH int | ||
| 350 | Stride int | 353 | Stride int | ||
| 351 | - | Opaque bool // xrgb8888: ignore the alpha byte | 354 | + | Opaque bool // x formats: ignore the alpha byte |
| 352 | Pixels []byte | 355 | 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. | ||