renderer.go RawHistoryFolder

1package vulkan
2
3import (
4	"fmt"
5	"image"
6	"math"
7	"os"
8	"runtime"
9	"strconv"
10	"unsafe"
11
12	"github.com/bnema/neferwl/internal/ports"
13
14	vk "github.com/bnema/purego-vulkan/vulkan"
15)
16
17// Renderer is owned by one goroutine; its methods must not be called concurrently.
18type Renderer struct {
19	width, height int
20	// queuePriority is the global priority the queue got: realtime, high
21	// or default.
22	queuePriority string
23	instance      vk.Instance
24	id            *vk.InstanceDispatch
25	device        vk.Device
26	dd            *vk.DeviceDispatch
27	queue         vk.Queue
28	// own is the SDR composition image; HDR uses hdrOwn and PQ targets.
29	own        target
30	hdrOwn     target // linear BT.709 in units of SDR reference white
31	targets    []*target
32	current    int
33	renderMods []uint64
34	hdrMods    []uint64
35	hdrNits    float64
36	hdr        hdrPass
37	hdrError   error
38	// hdrReadback is used only by the GPU test to permit transfer from a 10-bit target.
39	hdrReadback bool
40	physical    vk.PhysicalDevice
41	// One converted cursor image is kept; changes replace it in bounded memory.
42	cursorCache cursorConversion
43	// last is the target of the last frame: what captures copy.
44	last *target
45	// captures are the readback slots (capture.go), allocated on the
46	// first capture; capturePass converts HDR frames into captureSDR.
47	captures    []*captureSlot
48	capturePass hdrPass
49	captureSDR  target
50	sync        captureSync
51	pool        vk.CommandPool
52	memory      vk.PhysicalDeviceMemoryProperties
53	family      uint32
54	// Frames in flight (frame.go): frame is the last frame recorded,
55	// submitted the last submitted, completed the last known finished.
56	slots                       [frameSlots]frameSlot
57	frame, submitted, completed uint64
58	deferred                    []deferredFree
59	// syncFD: frames export a sync file (else Render waits for them).
60	syncFD bool
61	// dmabuf is what clients may send; imports are their buffers by ID.
62	dmabuf  ports.DMABufSupport
63	imports map[uint64]*imported
64	// pools are client wl_shm pools mapped by pool ID.
65	pools map[uint64]*mapping
66	// retired are pool mappings replaced this frame, unmapped once the
67	// frame's copies are done.
68	retired [][]byte
69	// shm are the GPU copies of wl_shm surfaces (shm.go); copied counts
70	// the bytes copied into them, for tests.
71	shm    map[shmKey]*shmSurface
72	copied int
73	// marks date the frames for Trim's wall-clock eviction (trim.go).
74	marks []trimMark
75	// redrawn counts the target pixels drawn, for tests.
76	redrawn int
77	// cursors are the exported cursor images (CursorBuffers).
78	cursors [2]*cursorImage
79	// compose draws every frame (compose.go); maxRange bounds one
80	// storage buffer binding.
81	compose  composer
82	maxRange int
83	// Scratch belongs to the renderer goroutine; neither slice is published.
84	scratchDraws  []draw
85	scratchCovers []image.Rectangle
86	damage        damageRegion
87	damageSeen    map[ports.WindowID]bool
88	damageDrawn   map[ports.WindowID]heldWindow
89	frameDMAs     []*imported
90	frameAcquires map[*imported]*os.File
91	frameWaits    []vk.Semaphore
92	frameStages   []vk.PipelineStageFlags
93}
94
95// priorityAttempts falls back to an ordinary queue when elevated priority is unavailable.
96func priorityAttempts(supported bool) []vk.QueueGlobalPriority {
97	if supported {
98		return []vk.QueueGlobalPriority{vk.QueueGlobalPriorityRealtime, vk.QueueGlobalPriorityHigh, 0}
99	}
100	return []vk.QueueGlobalPriority{0}
101}
102
103func checked(name string, result vk.Result) error {
104	if err := vk.Check(result); err != nil {
105		return fmt.Errorf("%s: %w", name, err)
106	}
107	return nil
108}
109
110func New(width, height int) (r *Renderer, err error) {
111	if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
112		return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
113	}
114	r = &Renderer{width: width, height: height, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}, shm: map[shmKey]*shmSurface{}}
115	defer func() {
116		if err != nil {
117			r.Close()
118			r = nil
119		}
120	}()
121	if err = vk.Init(); err != nil {
122		return nil, fmt.Errorf("vk.Init: %w", err)
123	}
124	app := vk.ApplicationInfo{SType: vk.StructureTypeApplicationInfo, ApiVersion: vk.MakeVersion(1, 3, 0)}
125	info := vk.InstanceCreateInfo{SType: vk.StructureTypeInstanceCreateInfo, ApplicationInfo: &app}
126	if err = checked("vkCreateInstance", vk.Global().CreateInstance(&info, nil, &r.instance)); err != nil {
127		return
128	}
129	r.id, err = vk.LoadInstanceDispatch(r.instance)
130	if err != nil {
131		err = fmt.Errorf("LoadInstanceDispatch: %w", err)
132		return
133	}
134	var count uint32
135	if err = checked("vkEnumeratePhysicalDevices(count)", r.id.EnumeratePhysicalDevices(r.instance, &count, nil)); err != nil {
136		return
137	}
138	if count == 0 {
139		err = fmt.Errorf("no Vulkan 1.3 device")
140		return
141	}
142	devices := make([]vk.PhysicalDevice, count)
143	if err = checked("vkEnumeratePhysicalDevices", r.id.EnumeratePhysicalDevices(r.instance, &count, &devices[0])); err != nil {
144		return
145	}
146	index := -1
147	if value, ok := os.LookupEnv("NEFERWL_VK_DEVICE"); ok {
148		index, err = strconv.Atoi(value)
149		if err != nil || index < 0 || index >= int(count) {
150			err = fmt.Errorf("invalid NEFERWL_VK_DEVICE index %q", value)
151			return
152		}
153	}
154	var physical vk.PhysicalDevice
155	var family uint32
156	for i, device := range devices[:count] {
157		if index >= 0 && i != index {
158			continue
159		}
160		var properties vk.PhysicalDeviceProperties
161		r.id.GetPhysicalDeviceProperties(device, &properties)
162		if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {
163			continue
164		}
165		var n uint32
166		r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)
167		if n == 0 {
168			continue
169		}
170		families := make([]vk.QueueFamilyProperties, n)
171		r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, &families[0])
172		for j, f := range families[:n] {
173			if f.QueueCount > 0 && f.QueueFlags&vk.QueueGraphicsBit != 0 {
174				physical = device
175				family = uint32(j)
176				break
177			}
178		}
179		if physical != 0 {
180			break
181		}
182	}
183	if physical == 0 {
184		err = fmt.Errorf("no Vulkan 1.3 device with a graphics queue")
185		return
186	}
187	r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
188	var properties vk.PhysicalDeviceProperties
189	r.id.GetPhysicalDeviceProperties(physical, &properties)
190	r.maxRange = int(properties.Limits.MaxStorageBufferRange)
191	r.family = family
192	priority := float32(1)
193	qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
194	dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1}
195	di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi}
196	// dmabuf import needs every extension; without them clients use wl_shm.
197	var extNames [][]byte
198	var extPtrs []*byte
199	have := r.extensions(physical)
200	dmabufSupported := true
201	for _, e := range deviceExtensions {
202		if !have[e] {
203			dmabufSupported = false
204		}
205	}
206	if dmabufSupported {
207		for _, e := range deviceExtensions {
208			extNames = append(extNames, append([]byte(e), 0))
209			extPtrs = append(extPtrs, &extNames[len(extNames)-1][0])
210		}
211		di.EnabledExtensionCount = uint32(len(extPtrs))
212		di.PpEnabledExtensionNames = &extPtrs[0]
213	}
214	globalExt := ""
215	if have[vk.KHRGlobalPriorityExtensionName] {
216		globalExt = vk.KHRGlobalPriorityExtensionName
217	} else if have[vk.EXTGlobalPriorityExtensionName] {
218		globalExt = vk.EXTGlobalPriorityExtensionName
219	}
220	var global vk.DeviceQueueGlobalPriorityCreateInfo
221	chosen := "default"
222	for _, attempt := range priorityAttempts(globalExt != "") {
223		qi.Next = nil
224		count := len(extPtrs)
225		if attempt != 0 {
226			global = vk.DeviceQueueGlobalPriorityCreateInfo{SType: vk.StructureTypeDeviceQueueGlobalPriorityCreateInfo, GlobalPriority: attempt}
227			qi.Next = unsafe.Pointer(&global)
228			extNames = append(extNames, append([]byte(globalExt), 0))
229			extPtrs = append(extPtrs, &extNames[len(extNames)-1][0])
230			chosen = "realtime"
231			if attempt == vk.QueueGlobalPriorityHigh {
232				chosen = "high"
233			}
234		} else {
235			chosen = "default"
236		}
237		di.EnabledExtensionCount = uint32(len(extPtrs))
238		if len(extPtrs) > 0 {
239			di.PpEnabledExtensionNames = &extPtrs[0]
240		} else {
241			di.PpEnabledExtensionNames = nil
242		}
243		result := r.id.CreateDevice(physical, &di, nil, &r.device)
244		runtime.KeepAlive(global)
245		runtime.KeepAlive(qi)
246		runtime.KeepAlive(extNames)
247		runtime.KeepAlive(extPtrs)
248		extPtrs = extPtrs[:count]
249		extNames = extNames[:count]
250		if result == vk.Success {
251			err = nil
252			break
253		}
254		err = checked("vkCreateDevice", result)
255		if attempt == 0 {
256			return
257		}
258	}
259	r.queuePriority = chosen
260	r.dd, err = vk.LoadDeviceDispatch(r.id, r.device)
261	if err != nil {
262		err = fmt.Errorf("LoadDeviceDispatch: %w", err)
263		return
264	}
265	runtime.KeepAlive(extNames)
266	runtime.KeepAlive(extPtrs)
267	if dmabufSupported {
268		// Clients need the render node to allocate on the right GPU.
269		if sup := r.probeDMABuf(physical); sup.Device != 0 {
270			r.dmabuf = sup
271			r.probeRenderModifiers(physical)
272			r.physical = physical
273		}
274		// VK_KHR_external_semaphore_fd (enabled above): frames export
275		// their fence as a sync file.
276		r.syncFD = r.dd.HasGetSemaphoreFdKHR()
277	}
278	r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
279	extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
280	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: targetUsage | vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
281	if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil {
282		return
283	}
284	var req vk.MemoryRequirements
285	r.dd.GetImageMemoryRequirements(r.device, r.own.image, &req)
286	var kind uint32
287	kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
288	if err != nil {
289		return
290	}
291	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
292	if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.own.memory)); err != nil {
293		return
294	}
295	if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.own.image, r.own.memory, 0)); err != nil {
296		return
297	}
298	pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}
299	if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {
300		return
301	}
302	if err = r.createSlots(); err != nil {
303		return
304	}
305	if err = r.createView(&r.own); err != nil {
306		return
307	}
308	if err = r.createComposer(); err != nil {
309		return
310	}
311	runtime.KeepAlive(priority)
312	return
313}
314
315func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (uint32, error) {
316	for i := uint32(0); i < r.memory.MemoryTypeCount; i++ {
317		if bits&(uint32(1)<<i) != 0 && r.memory.MemoryTypes[i].PropertyFlags&props == props {
318			return i, nil
319		}
320	}
321	return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
322}
323
324// CopiedBytes reports cumulative wl_shm bytes copied into GPU storage.
325// It is diagnostic only and must be read by the renderer's owner goroutine.
326func (r *Renderer) CopiedBytes() int { return r.copied }
327
328// QueuePriority is the global priority the queue got: realtime, high or
329// default.
330func (r *Renderer) QueuePriority() string { return r.queuePriority }
331
332// Pixels reads the last frame back through a capture slot, waiting for
333// the GPU on the calling goroutine (slot fence, no sync file needed):
334// headless screenshots and tests only. Nil when no slot is free (both
335// leased to captures) or the copy failed: never a silent black image.
336func (r *Renderer) Pixels() *image.RGBA {
337	cf, err := r.debugCapture()
338	if err != nil {
339		return nil
340	}
341	defer r.EndCapture(cf)
342	out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
343	if err := cf.Read(image.Rect(0, 0, r.width, r.height), out.Pix, out.Stride); err != nil {
344		return nil
345	}
346	for i := 0; i < len(out.Pix); i += 4 {
347		out.Pix[i], out.Pix[i+2] = out.Pix[i+2], out.Pix[i]
348	}
349	return out
350}
351
352// Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE.
353const (
354	queueFamilyIgnored = ^uint32(0)
355	wholeSize          = ^vk.DeviceSize(0)
356)
357
358func (r *Renderer) Close() {
359	if r == nil {
360		return
361	}
362	if r.dd != nil {
363		d := r.dd
364		if r.device != 0 {
365			_ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
366			r.idle()
367		}
368		r.dropTargets()
369		r.destroyHDR()
370		r.dropCursors()
371		for id, im := range r.imports {
372			r.release(im)
373			delete(r.imports, id)
374		}
375		r.frame += importTTL + 1
376		r.dropPools()
377		r.dropShm()
378		r.destroySlots()
379		r.destroyCaptures()
380		if r.pool != 0 {
381			d.DestroyCommandPool(r.device, r.pool, nil)
382			r.pool = 0
383		}
384		r.destroyComposer()
385		r.freeTarget(&r.hdrOwn)
386		r.freeTarget(&r.own)
387		d.DestroyDevice(r.device, nil)
388		r.device = 0
389	} else if r.device != 0 && r.id != nil {
390		if vk.VkDestroyDevice != nil {
391			vk.VkDestroyDevice(r.device, nil)
392		}
393		r.device = 0
394	}
395	if r.instance != 0 {
396		if r.id != nil {
397			r.id.DestroyInstance(r.instance, nil)
398		} else if vk.VkDestroyInstance != nil {
399			vk.VkDestroyInstance(r.instance, nil)
400		}
401		r.instance = 0
402	}
403}

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 25

Tags 10

↑↓ moveenter openesc close