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}
59
60func Open(main Identity) (_ *Device, err error) {
61 if err = vulkan.Init(); err != nil {
62 return nil, err
63 }
64 app := append([]byte("NeferGUI"), 0)
65 ai := vulkan.ApplicationInfo{SType: vulkan.StructureTypeApplicationInfo, ApplicationName: &app[0], ApiVersion: (1 << 22) | (3 << 12)}
66 ci := vulkan.InstanceCreateInfo{SType: vulkan.StructureTypeInstanceCreateInfo, ApplicationInfo: &ai}
67 var v *Validation
68 var layer, extension []byte
69 var layerPtr, extensionPtr *byte
70 if os.Getenv("NEFERGUI_VULKAN_VALIDATION") == "1" {
71 v = &Validation{}
72 if _, _, err = v.enabled(); err != nil {
73 return nil, err
74 }
75 layer = append([]byte(validationLayer), 0)
76 extension = append([]byte(debugExtension), 0)
77 layerPtr, extensionPtr = &layer[0], &extension[0]
78 ci.EnabledLayerCount = 1
79 ci.PpEnabledLayerNames = &layerPtr
80 ci.EnabledExtensionCount = 1
81 ci.PpEnabledExtensionNames = &extensionPtr
82 }
83 var instance vulkan.Instance
84 if err = vulkan.Check(vulkan.Global().CreateInstance(&ci, nil, &instance)); err != nil {
85 return nil, fmt.Errorf("create Vulkan 1.3 instance: %w", err)
86 }
87 id, e := vulkan.LoadInstanceDispatch(instance)
88 if e != nil {
89 vulkan.VkDestroyInstance(instance, nil)
90 return nil, e
91 }
92 d := &Device{Instance: instance, InstanceDispatch: id, Validation: v}
93 if v != nil {
94 if e = v.create(id, instance); e != nil {
95 d.Close()
96 return nil, e
97 }
98 }
99 defer func() {
100 if err != nil {
101 d.Close()
102 }
103 }()
104 var count uint32
105 if err = vulkan.Check(id.EnumeratePhysicalDevices(instance, &count, nil)); err != nil {
106 return nil, err
107 }
108 if count == 0 {
109 return nil, fmt.Errorf("no Vulkan physical devices; need DRM main_device %d:%d", main.Major, main.Minor)
110 }
111 physical := make([]vulkan.PhysicalDevice, count)
112 if err = vulkan.Check(id.EnumeratePhysicalDevices(instance, &count, &physical[0])); err != nil {
113 return nil, err
114 }
115 for _, candidate := range physical[:count] {
116 exts, e := extensions(id, candidate)
117 if e != nil {
118 return nil, e
119 }
120 if !exts["VK_EXT_physical_device_drm"] {
121 continue
122 }
123 props := vulkan.PhysicalDeviceProperties2{SType: vulkan.StructureTypePhysicalDeviceProperties2}
124 drm := vulkan.PhysicalDeviceDrmPropertiesEXT{SType: vulkan.StructureTypePhysicalDeviceDRMPropertiesEXT}
125 props.Next = unsafe.Pointer(&drm)
126 id.GetPhysicalDeviceProperties2(candidate, &props)
127 if !Matches(main, Identity{uint32(drm.PrimaryMajor), uint32(drm.PrimaryMinor)}, Identity{uint32(drm.RenderMajor), uint32(drm.RenderMinor)}, drm.HasPrimary != 0, drm.HasRender != 0) {
128 continue
129 }
130 if drm.HasRender == 0 {
131 return nil, fmt.Errorf("vulkan device matching main_device %d:%d has no render node", main.Major, main.Minor)
132 }
133 for _, required := range RequiredExtensions {
134 if !exts[required] {
135 return nil, fmt.Errorf("matching Vulkan device missing %s", required)
136 }
137 }
138 d.Physical = candidate
139 d.Render = Identity{uint32(drm.RenderMajor), uint32(drm.RenderMinor)}
140 break
141 }
142 if d.Physical == 0 {
143 return nil, fmt.Errorf("no Vulkan DRM device matches linux-dmabuf main_device %d:%d (no cross-GPU fallback)", main.Major, main.Minor)
144 }
145 features := vulkan.PhysicalDeviceFeatures2{SType: vulkan.StructureTypePhysicalDeviceFeatures2}
146 timeline := vulkan.PhysicalDeviceTimelineSemaphoreFeatures{SType: vulkan.StructureTypePhysicalDeviceTimelineSemaphoreFeatures}
147 sync2 := vulkan.PhysicalDeviceSynchronization2Features{SType: vulkan.StructureTypePhysicalDeviceSynchronization2Features}
148 dynamic := vulkan.PhysicalDeviceDynamicRenderingFeatures{SType: vulkan.StructureTypePhysicalDeviceDynamicRenderingFeatures}
149 features.Next = unsafe.Pointer(&timeline)
150 timeline.Next = unsafe.Pointer(&sync2)
151 sync2.Next = unsafe.Pointer(&dynamic)
152 id.GetPhysicalDeviceFeatures2(d.Physical, &features)
153 if timeline.TimelineSemaphore == 0 || sync2.Synchronization2 == 0 || dynamic.DynamicRendering == 0 {
154 return nil, fmt.Errorf("matching Vulkan device missing timeline/synchronization2/dynamic rendering feature")
155 }
156 var families uint32
157 id.GetPhysicalDeviceQueueFamilyProperties(d.Physical, &families, nil)
158 if families == 0 {
159 return nil, fmt.Errorf("matching Vulkan device has no graphics queue")
160 }
161 queues := make([]vulkan.QueueFamilyProperties, families)
162 id.GetPhysicalDeviceQueueFamilyProperties(d.Physical, &families, &queues[0])
163 found := false
164 for i, q := range queues[:families] {
165 if q.QueueCount != 0 && q.QueueFlags&vulkan.QueueGraphicsBit != 0 {
166 d.QueueFamily = uint32(i)
167 found = true
168 break
169 }
170 }
171 if !found {
172 return nil, fmt.Errorf("matching Vulkan device has no graphics queue")
173 }
174 priority := float32(1)
175 qci := vulkan.DeviceQueueCreateInfo{SType: vulkan.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: d.QueueFamily, QueueCount: 1, QueuePriorities: &priority}
176 // Enable only the extensions actually used; core 1.3 features still need
177 // their feature-chain Boolean set explicitly at vkCreateDevice.
178 names := make([][]byte, 0, len(RequiredExtensions)-1)
179 ptrs := make([]*byte, 0, len(RequiredExtensions)-1)
180 for _, name := range RequiredExtensions {
181 if name == "VK_EXT_physical_device_drm" {
182 continue
183 }
184 names = append(names, append([]byte(name), 0))
185 ptrs = append(ptrs, &names[len(names)-1][0])
186 }
187 dci := vulkan.DeviceCreateInfo{SType: vulkan.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&timeline), QueueCreateInfoCount: 1, QueueCreateInfos: &qci, EnabledExtensionCount: uint32(len(ptrs)), PpEnabledExtensionNames: &ptrs[0]}
188 if err = vulkan.Check(id.CreateDevice(d.Physical, &dci, nil, &d.Logical)); err != nil {
189 return nil, fmt.Errorf("create matched Vulkan device: %w", err)
190 }
191 d.Dispatch, err = vulkan.LoadDeviceDispatch(id, d.Logical)
192 if err != nil {
193 vulkan.VkDestroyDevice(d.Logical, nil)
194 d.Logical = 0
195 return nil, err
196 }
197 d.Dispatch.GetDeviceQueue(d.Logical, d.QueueFamily, 0, &d.Queue)
198 runtime.KeepAlive(app)
199 runtime.KeepAlive(layer)
200 runtime.KeepAlive(extension)
201 runtime.KeepAlive(names)
202 runtime.KeepAlive(ptrs)
203 return d, nil
204}
205
206func (d *Device) Close() {
207 if d == nil {
208 return
209 }
210 if d.Logical != 0 {
211 if d.Dispatch != nil {
212 _ = vulkan.Check(d.Dispatch.DeviceWaitIdle(d.Logical))
213 if d.atlas != nil {
214 d.atlas.Close()
215 d.atlas = nil
216 }
217 if d.images != nil {
218 d.images.Close()
219 d.images = nil
220 }
221 d.Dispatch.DestroyDevice(d.Logical, nil)
222 }
223 d.Logical = 0
224 }
225 if d.Instance != 0 {
226 if d.Validation != nil {
227 d.Validation.destroy(d.InstanceDispatch, d.Instance)
228 }
229 d.InstanceDispatch.DestroyInstance(d.Instance, nil)
230 d.Instance = 0
231 }
232}
233
234func extensions(id *vulkan.InstanceDispatch, physical vulkan.PhysicalDevice) (map[string]bool, error) {
235 var count uint32
236 if err := vulkan.Check(id.EnumerateDeviceExtensionProperties(physical, nil, &count, nil)); err != nil {
237 return nil, err
238 }
239 found := map[string]bool{}
240 if count == 0 {
241 return found, nil
242 }
243 props := make([]vulkan.ExtensionProperties, count)
244 if err := vulkan.Check(id.EnumerateDeviceExtensionProperties(physical, nil, &count, &props[0])); err != nil {
245 return nil, err
246 }
247 for _, p := range props[:count] {
248 found[string(bytes.TrimRight(p.ExtensionName[:], "\x00"))] = true
249 }
250 return found, nil
251}