v0.1.0 47aaa0f

device.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close