1//go:build linux
2
3// Package vkdevice selects a Vulkan graphics device by compositor-advertised
4// DRM dev_t. It rejects cross-GPU fallback rather than silently copying images.
5package vkdevice
6
7import (
8 "bytes"
9 "encoding/binary"
10 "fmt"
11 "os"
12 "runtime"
13 "unsafe"
14
15 "github.com/bnema/purego-vulkan/vulkan"
16)
17
18// Identity uses the Linux gnu_dev_major/minor dev_t encoding. A Wayland
19// main_device is an 8-byte native-endian dev_t, not a Vulkan device index.
20type Identity struct{ Major, Minor uint32 }
21
22func DecodeDevice(device []byte) (Identity, error) {
23 if len(device) != 8 {
24 return Identity{}, fmt.Errorf("invalid main_device dev_t length %d", len(device))
25 }
26 d := binary.NativeEndian.Uint64(device)
27 return Identity{Major: uint32((d >> 8) & 0xfff), Minor: uint32((d & 0xff) | ((d >> 12) & 0xffffff00))}, nil
28}
29func DeviceIdentity(device uint64) Identity {
30 return Identity{Major: uint32((device >> 8) & 0xfff), Minor: uint32((device & 0xff) | ((device >> 12) & 0xffffff00))}
31}
32func Matches(main Identity, primary, render Identity, hasPrimary, hasRender bool) bool {
33 return (hasPrimary && main == primary) || (hasRender && main == render)
34}
35
36// RequiredExtensions are mandatory on the matched device. Vulkan 1.3
37// supplies dynamic rendering and synchronization2 as core features.
38var RequiredExtensions = []string{
39 "VK_EXT_physical_device_drm",
40 "VK_KHR_external_memory_fd",
41 "VK_EXT_external_memory_dma_buf",
42 "VK_EXT_image_drm_format_modifier",
43 "VK_KHR_external_semaphore_fd",
44}
45
46type Device struct {
47 Instance vulkan.Instance
48 InstanceDispatch *vulkan.InstanceDispatch
49 Physical vulkan.PhysicalDevice
50 Logical vulkan.Device
51 Dispatch *vulkan.DeviceDispatch
52 Queue vulkan.Queue
53 QueueFamily uint32
54 Render Identity
55 Validation *Validation
56 atlas *glyphAtlas
57 images *imageCache
58 imageDimension int // application ceiling clamped to the selected device
59}
60
61func Open(main Identity) (_ *Device, err error) {
62 if err = vulkan.Init(); err != nil {
63 return nil, err
64 }
65 app := append([]byte("NeferGUI"), 0)
66 ai := vulkan.ApplicationInfo{SType: vulkan.StructureTypeApplicationInfo, ApplicationName: &app[0], ApiVersion: (1 << 22) | (3 << 12)}
67 ci := vulkan.InstanceCreateInfo{SType: vulkan.StructureTypeInstanceCreateInfo, ApplicationInfo: &ai}
68 var v *Validation
69 var layer, extension []byte
70 var layerPtr, extensionPtr *byte
71 if os.Getenv("NEFERGUI_VULKAN_VALIDATION") == "1" {
72 v = &Validation{}
73 if _, _, err = v.enabled(); err != nil {
74 return nil, err
75 }
76 layer = append([]byte(validationLayer), 0)
77 extension = append([]byte(debugExtension), 0)
78 layerPtr, extensionPtr = &layer[0], &extension[0]
79 ci.EnabledLayerCount = 1
80 ci.PpEnabledLayerNames = &layerPtr
81 ci.EnabledExtensionCount = 1
82 ci.PpEnabledExtensionNames = &extensionPtr
83 }
84 var instance vulkan.Instance
85 if err = vulkan.Check(vulkan.Global().CreateInstance(&ci, nil, &instance)); err != nil {
86 return nil, fmt.Errorf("create Vulkan 1.3 instance: %w", err)
87 }
88 id, e := vulkan.LoadInstanceDispatch(instance)
89 if e != nil {
90 vulkan.VkDestroyInstance(instance, nil)
91 return nil, e
92 }
93 d := &Device{Instance: instance, InstanceDispatch: id, Validation: v}
94 if v != nil {
95 if e = v.create(id, instance); e != nil {
96 d.Close()
97 return nil, e
98 }
99 }
100 defer func() {
101 if err != nil {
102 d.Close()
103 }
104 }()
105 var count uint32
106 if err = vulkan.Check(id.EnumeratePhysicalDevices(instance, &count, nil)); err != nil {
107 return nil, err
108 }
109 if count == 0 {
110 return nil, fmt.Errorf("no Vulkan physical devices; need DRM main_device %d:%d", main.Major, main.Minor)
111 }
112 physical := make([]vulkan.PhysicalDevice, count)
113 if err = vulkan.Check(id.EnumeratePhysicalDevices(instance, &count, &physical[0])); err != nil {
114 return nil, err
115 }
116 for _, candidate := range physical[:count] {
117 exts, e := extensions(id, candidate)
118 if e != nil {
119 return nil, e
120 }
121 if !exts["VK_EXT_physical_device_drm"] {
122 continue
123 }
124 props := vulkan.PhysicalDeviceProperties2{SType: vulkan.StructureTypePhysicalDeviceProperties2}
125 drm := vulkan.PhysicalDeviceDrmPropertiesEXT{SType: vulkan.StructureTypePhysicalDeviceDRMPropertiesEXT}
126 props.Next = unsafe.Pointer(&drm)
127 id.GetPhysicalDeviceProperties2(candidate, &props)
128 if !Matches(main, Identity{uint32(drm.PrimaryMajor), uint32(drm.PrimaryMinor)}, Identity{uint32(drm.RenderMajor), uint32(drm.RenderMinor)}, drm.HasPrimary != 0, drm.HasRender != 0) {
129 continue
130 }
131 if drm.HasRender == 0 {
132 return nil, fmt.Errorf("vulkan device matching main_device %d:%d has no render node", main.Major, main.Minor)
133 }
134 for _, required := range RequiredExtensions {
135 if !exts[required] {
136 return nil, fmt.Errorf("matching Vulkan device missing %s", required)
137 }
138 }
139 d.Physical = candidate
140 d.imageDimension = int(min(uint32(maxImageDimension), props.Properties.Limits.MaxImageDimension2D))
141 d.Render = Identity{uint32(drm.RenderMajor), uint32(drm.RenderMinor)}
142 break
143 }
144 if d.Physical == 0 {
145 return nil, fmt.Errorf("no Vulkan DRM device matches linux-dmabuf main_device %d:%d (no cross-GPU fallback)", main.Major, main.Minor)
146 }
147 features := vulkan.PhysicalDeviceFeatures2{SType: vulkan.StructureTypePhysicalDeviceFeatures2}
148 timeline := vulkan.PhysicalDeviceTimelineSemaphoreFeatures{SType: vulkan.StructureTypePhysicalDeviceTimelineSemaphoreFeatures}
149 sync2 := vulkan.PhysicalDeviceSynchronization2Features{SType: vulkan.StructureTypePhysicalDeviceSynchronization2Features}
150 dynamic := vulkan.PhysicalDeviceDynamicRenderingFeatures{SType: vulkan.StructureTypePhysicalDeviceDynamicRenderingFeatures}
151 features.Next = unsafe.Pointer(&timeline)
152 timeline.Next = unsafe.Pointer(&sync2)
153 sync2.Next = unsafe.Pointer(&dynamic)
154 id.GetPhysicalDeviceFeatures2(d.Physical, &features)
155 if timeline.TimelineSemaphore == 0 || sync2.Synchronization2 == 0 || dynamic.DynamicRendering == 0 {
156 return nil, fmt.Errorf("matching Vulkan device missing timeline/synchronization2/dynamic rendering feature")
157 }
158 var families uint32
159 id.GetPhysicalDeviceQueueFamilyProperties(d.Physical, &families, nil)
160 if families == 0 {
161 return nil, fmt.Errorf("matching Vulkan device has no graphics queue")
162 }
163 queues := make([]vulkan.QueueFamilyProperties, families)
164 id.GetPhysicalDeviceQueueFamilyProperties(d.Physical, &families, &queues[0])
165 found := false
166 for i, q := range queues[:families] {
167 if q.QueueCount != 0 && q.QueueFlags&vulkan.QueueGraphicsBit != 0 {
168 d.QueueFamily = uint32(i)
169 found = true
170 break
171 }
172 }
173 if !found {
174 return nil, fmt.Errorf("matching Vulkan device has no graphics queue")
175 }
176 priority := float32(1)
177 qci := vulkan.DeviceQueueCreateInfo{SType: vulkan.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: d.QueueFamily, QueueCount: 1, QueuePriorities: &priority}
178 // Enable only the extensions actually used; core 1.3 features still need
179 // their feature-chain Boolean set explicitly at vkCreateDevice.
180 names := make([][]byte, 0, len(RequiredExtensions)-1)
181 ptrs := make([]*byte, 0, len(RequiredExtensions)-1)
182 for _, name := range RequiredExtensions {
183 if name == "VK_EXT_physical_device_drm" {
184 continue
185 }
186 names = append(names, append([]byte(name), 0))
187 ptrs = append(ptrs, &names[len(names)-1][0])
188 }
189 dci := vulkan.DeviceCreateInfo{SType: vulkan.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&timeline), QueueCreateInfoCount: 1, QueueCreateInfos: &qci, EnabledExtensionCount: uint32(len(ptrs)), PpEnabledExtensionNames: &ptrs[0]}
190 if err = vulkan.Check(id.CreateDevice(d.Physical, &dci, nil, &d.Logical)); err != nil {
191 return nil, fmt.Errorf("create matched Vulkan device: %w", err)
192 }
193 d.Dispatch, err = vulkan.LoadDeviceDispatch(id, d.Logical)
194 if err != nil {
195 vulkan.VkDestroyDevice(d.Logical, nil)
196 d.Logical = 0
197 return nil, err
198 }
199 d.Dispatch.GetDeviceQueue(d.Logical, d.QueueFamily, 0, &d.Queue)
200 runtime.KeepAlive(app)
201 runtime.KeepAlive(layer)
202 runtime.KeepAlive(extension)
203 runtime.KeepAlive(names)
204 runtime.KeepAlive(ptrs)
205 return d, nil
206}
207
208func (d *Device) Close() {
209 if d == nil {
210 return
211 }
212 if d.Logical != 0 {
213 if d.Dispatch != nil {
214 _ = vulkan.Check(d.Dispatch.DeviceWaitIdle(d.Logical))
215 if d.atlas != nil {
216 d.atlas.Close()
217 d.atlas = nil
218 }
219 if d.images != nil {
220 d.images.Close()
221 d.images = nil
222 }
223 d.Dispatch.DestroyDevice(d.Logical, nil)
224 }
225 d.Logical = 0
226 }
227 if d.Instance != 0 {
228 if d.Validation != nil {
229 d.Validation.destroy(d.InstanceDispatch, d.Instance)
230 }
231 d.InstanceDispatch.DestroyInstance(d.Instance, nil)
232 d.Instance = 0
233 }
234}
235
236func extensions(id *vulkan.InstanceDispatch, physical vulkan.PhysicalDevice) (map[string]bool, error) {
237 var count uint32
238 if err := vulkan.Check(id.EnumerateDeviceExtensionProperties(physical, nil, &count, nil)); err != nil {
239 return nil, err
240 }
241 found := map[string]bool{}
242 if count == 0 {
243 return found, nil
244 }
245 props := make([]vulkan.ExtensionProperties, count)
246 if err := vulkan.Check(id.EnumerateDeviceExtensionProperties(physical, nil, &count, &props[0])); err != nil {
247 return nil, err
248 }
249 for _, p := range props[:count] {
250 found[string(bytes.TrimRight(p.ExtensionName[:], "\x00"))] = true
251 }
252 return found, nil
253}