feat/hint-hybrid aed7ca4

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/adapters/hintlabel"
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/bnema/zerowrap"
15
16	vk "github.com/bnema/purego-vulkan/vulkan"
17)
18
19// Renderer is owned by one goroutine; its methods must not be called concurrently.
20type Renderer struct {
21	width, height int
22	// queuePriority is the global priority the queue got: realtime, high
23	// or default.
24	queuePriority string
25	instance      vk.Instance
26	id            *vk.InstanceDispatch
27	device        vk.Device
28	dd            *vk.DeviceDispatch
29	queue         vk.Queue
30	// own is the SDR composition image; HDR uses hdrOwn and PQ targets.
31	own        target
32	hdrOwn     target // linear BT.709 in units of SDR reference white
33	targets    []*target
34	current    int
35	renderMods []uint64
36	hdrMods    []uint64
37	hdrNits    float64
38	hdr        hdrPass
39	hdrError   error
40	// modPlanes is the memory plane count of each probed target modifier
41	// (SDR and HDR): DCC modifiers have more than one.
42	modPlanes map[uint64]uint32
43	// log reports the chosen target modifier; the zero value discards.
44	log zerowrap.Logger
45	// traceDamage logs every frame's redrawn area (--debug=render-damage).
46	traceDamage bool
47	// hdrReadback makes exported HDR targets transfer sources, for GPU tests
48	// and headless --screenshot-raw (SetHDRReadback); never DRM.
49	hdrReadback bool
50	// virtual: the output has no display (headless), so ExportTargets
51	// accepts an empty modifier list for HDR (SetVirtualOutput).
52	virtual  bool
53	physical vk.PhysicalDevice
54	// One converted cursor image is kept; changes replace it in bounded memory.
55	cursorCache cursorConversion
56	// lastHDRTarget is the index in targets of the exported PQ target the
57	// last HDR frame rendered into; -1 when the current targets hold none.
58	lastHDRTarget int
59	// last is the target of the last frame: what captures copy.
60	last *target
61	// captures are the readback slots (capture.go), allocated on the
62	// first capture; capturePass converts HDR frames into captureSDR.
63	captures    []*captureSlot
64	capturePass hdrPass
65	captureSDR  target
66	sync        captureSync
67	pool        vk.CommandPool
68	memory      vk.PhysicalDeviceMemoryProperties
69	family      uint32
70	// Frames in flight (frame.go): frame is the last frame recorded,
71	// submitted the last submitted, completed the last known finished.
72	slots                       [frameSlots]frameSlot
73	frame, submitted, completed uint64
74	deferred                    []deferredFree
75	// syncFD: frames export a sync file (else Render waits for them).
76	syncFD bool
77	// dmabuf is what clients may send; imports are their buffers by ID.
78	dmabuf  ports.DMABufSupport
79	imports map[uint64]*imported
80	// pools are client wl_shm pools mapped by pool ID.
81	pools map[uint64]*mapping
82	// retired are pool mappings replaced this frame, unmapped once the
83	// frame's copies are done.
84	retired [][]byte
85	// shm are the GPU copies of wl_shm surfaces (shm.go); copied counts
86	// the bytes copied into them, for tests.
87	shm    map[shmKey]*shmSurface
88	copied int
89	// labels rasterizes the hint labels (hint_labels.go); the zero value works.
90	labels hintlabel.Raster
91	// shmRects is scratch for shmCopyFor.
92	shmRects []image.Rectangle
93	// marks date the frames for Trim's wall-clock eviction (trim.go).
94	marks []trimMark
95	// redrawn counts the target pixels drawn (TakeRedrawn).
96	redrawn int
97	// fullRedraws counts why whole targets or windows were redrawn; each
98	// TakeRedrawn logs and resets it.
99	fullRedraws fullRedraws
100	// cursors are the exported cursor images (CursorBuffers).
101	cursors [2]*cursorImage
102	// compose draws every frame (compose.go); maxRange bounds one
103	// storage buffer binding.
104	compose  composer
105	maxRange int
106	// Scratch belongs to the renderer goroutine; neither slice is published.
107	scratchDraws  []draw
108	scratchCovers []image.Rectangle
109	damage        damageRegion
110	damageSeen    map[ports.WindowID]bool
111	damageDrawn   map[ports.WindowID]heldWindow
112	frameDMAs     []*imported
113	frameAcquires map[*imported]*os.File
114	frameWaits    []vk.Semaphore
115	frameStages   []vk.PipelineStageFlags
116	// call holds the structs a per-frame Vulkan call is handed by pointer.
117	call callScratch
118	// capCall holds the structs a capture hands Vulkan by pointer.
119	capCall captureScratch
120}
121
122// priorityAttempts falls back to an ordinary queue when elevated priority is unavailable.
123func priorityAttempts(supported bool) []vk.QueueGlobalPriority {
124	if supported {
125		return []vk.QueueGlobalPriority{vk.QueueGlobalPriorityRealtime, vk.QueueGlobalPriorityHigh, 0}
126	}
127	return []vk.QueueGlobalPriority{0}
128}
129
130func checked(name string, result vk.Result) error {
131	if err := vk.Check(result); err != nil {
132		return fmt.Errorf("%s: %w", name, err)
133	}
134	return nil
135}
136
137func New(width, height int) (r *Renderer, err error) {
138	if width <= 0 || height <= 0 || width > math.MaxUint32 || height > math.MaxUint32 || uint64(width)*uint64(height) > uint64(math.MaxInt/4) {
139		return nil, fmt.Errorf("invalid renderer dimensions %d x %d", width, height)
140	}
141	r = &Renderer{width: width, height: height, lastHDRTarget: -1, imports: map[uint64]*imported{}, pools: map[uint64]*mapping{}, shm: map[shmKey]*shmSurface{}}
142	defer func() {
143		if err != nil {
144			r.Close()
145			r = nil
146		}
147	}()
148	if err = vk.Init(); err != nil {
149		return nil, fmt.Errorf("vk.Init: %w", err)
150	}
151	app := vk.ApplicationInfo{SType: vk.StructureTypeApplicationInfo, ApiVersion: vk.MakeVersion(1, 3, 0)}
152	info := vk.InstanceCreateInfo{SType: vk.StructureTypeInstanceCreateInfo, ApplicationInfo: &app}
153	if err = checked("vkCreateInstance", vk.Global().CreateInstance(&info, nil, &r.instance)); err != nil {
154		return
155	}
156	r.id, err = vk.LoadInstanceDispatch(r.instance)
157	if err != nil {
158		err = fmt.Errorf("LoadInstanceDispatch: %w", err)
159		return
160	}
161	var count uint32
162	if err = checked("vkEnumeratePhysicalDevices(count)", r.id.EnumeratePhysicalDevices(r.instance, &count, nil)); err != nil {
163		return
164	}
165	if count == 0 {
166		err = fmt.Errorf("no Vulkan 1.3 device")
167		return
168	}
169	devices := make([]vk.PhysicalDevice, count)
170	if err = checked("vkEnumeratePhysicalDevices", r.id.EnumeratePhysicalDevices(r.instance, &count, &devices[0])); err != nil {
171		return
172	}
173	index := -1
174	if value, ok := os.LookupEnv("NEFERWL_VK_DEVICE"); ok {
175		index, err = strconv.Atoi(value)
176		if err != nil || index < 0 || index >= int(count) {
177			err = fmt.Errorf("invalid NEFERWL_VK_DEVICE index %q", value)
178			return
179		}
180	}
181	var physical vk.PhysicalDevice
182	var family uint32
183	for i, device := range devices[:count] {
184		if index >= 0 && i != index {
185			continue
186		}
187		var properties vk.PhysicalDeviceProperties
188		r.id.GetPhysicalDeviceProperties(device, &properties)
189		if properties.ApiVersion < vk.MakeVersion(1, 3, 0) {
190			continue
191		}
192		var n uint32
193		r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, nil)
194		if n == 0 {
195			continue
196		}
197		families := make([]vk.QueueFamilyProperties, n)
198		r.id.GetPhysicalDeviceQueueFamilyProperties(device, &n, &families[0])
199		for j, f := range families[:n] {
200			if f.QueueCount > 0 && f.QueueFlags&vk.QueueGraphicsBit != 0 {
201				physical = device
202				family = uint32(j)
203				break
204			}
205		}
206		if physical != 0 {
207			break
208		}
209	}
210	if physical == 0 {
211		err = fmt.Errorf("no Vulkan 1.3 device with a graphics queue")
212		return
213	}
214	r.id.GetPhysicalDeviceMemoryProperties(physical, &r.memory)
215	var properties vk.PhysicalDeviceProperties
216	r.id.GetPhysicalDeviceProperties(physical, &properties)
217	r.maxRange = int(properties.Limits.MaxStorageBufferRange)
218	r.family = family
219	priority := float32(1)
220	qi := vk.DeviceQueueCreateInfo{SType: vk.StructureTypeDeviceQueueCreateInfo, QueueFamilyIndex: family, QueueCount: 1, QueuePriorities: &priority}
221	dynamicRendering := vk.PhysicalDeviceDynamicRenderingFeatures{SType: vk.StructureTypePhysicalDeviceDynamicRenderingFeatures, DynamicRendering: 1}
222	indexing := descriptorIndexingFeatures{SType: descriptorIndexingFeaturesType}
223	features := vk.PhysicalDeviceFeatures2{SType: vk.StructureTypePhysicalDeviceFeatures2, Next: unsafe.Pointer(&indexing)}
224	r.id.GetPhysicalDeviceFeatures2(physical, &features)
225	r.compose.resources = 1
226	coreFeatures := vk.PhysicalDeviceFeatures{}
227	if features.Features.ShaderSampledImageArrayDynamicIndexing != 0 && features.Features.ShaderStorageBufferArrayDynamicIndexing != 0 && indexing.ShaderSampledImageArrayNonUniformIndexing != 0 && indexing.ShaderStorageBufferArrayNonUniformIndexing != 0 {
228		limit := composeResourceLimit(properties.Limits)
229		if limit > 1 {
230			r.compose.resources, r.compose.indexing = limit, true
231			coreFeatures.ShaderSampledImageArrayDynamicIndexing = 1
232			coreFeatures.ShaderStorageBufferArrayDynamicIndexing = 1
233			// Enable only the array indexing features used by these shaders.
234			indexing = descriptorIndexingFeatures{SType: descriptorIndexingFeaturesType, ShaderSampledImageArrayNonUniformIndexing: 1, ShaderStorageBufferArrayNonUniformIndexing: 1}
235			dynamicRendering.Next = unsafe.Pointer(&indexing)
236		}
237	}
238	di := vk.DeviceCreateInfo{SType: vk.StructureTypeDeviceCreateInfo, Next: unsafe.Pointer(&dynamicRendering), QueueCreateInfoCount: 1, QueueCreateInfos: &qi, EnabledFeatures: &coreFeatures}
239	// dmabuf import needs every extension; without them clients use wl_shm.
240	var extNames [][]byte
241	var extPtrs []*byte
242	have := r.extensions(physical)
243	dmabufSupported := true
244	for _, e := range deviceExtensions {
245		if !have[e] {
246			dmabufSupported = false
247		}
248	}
249	if dmabufSupported {
250		for _, e := range deviceExtensions {
251			extNames = append(extNames, append([]byte(e), 0))
252			extPtrs = append(extPtrs, &extNames[len(extNames)-1][0])
253		}
254		di.EnabledExtensionCount = uint32(len(extPtrs))
255		di.PpEnabledExtensionNames = &extPtrs[0]
256	}
257	globalExt := ""
258	if have[vk.KHRGlobalPriorityExtensionName] {
259		globalExt = vk.KHRGlobalPriorityExtensionName
260	} else if have[vk.EXTGlobalPriorityExtensionName] {
261		globalExt = vk.EXTGlobalPriorityExtensionName
262	}
263	var global vk.DeviceQueueGlobalPriorityCreateInfo
264	chosen := "default"
265	for _, attempt := range priorityAttempts(globalExt != "") {
266		qi.Next = nil
267		count := len(extPtrs)
268		if attempt != 0 {
269			global = vk.DeviceQueueGlobalPriorityCreateInfo{SType: vk.StructureTypeDeviceQueueGlobalPriorityCreateInfo, GlobalPriority: attempt}
270			qi.Next = unsafe.Pointer(&global)
271			extNames = append(extNames, append([]byte(globalExt), 0))
272			extPtrs = append(extPtrs, &extNames[len(extNames)-1][0])
273			chosen = "realtime"
274			if attempt == vk.QueueGlobalPriorityHigh {
275				chosen = "high"
276			}
277		} else {
278			chosen = "default"
279		}
280		di.EnabledExtensionCount = uint32(len(extPtrs))
281		if len(extPtrs) > 0 {
282			di.PpEnabledExtensionNames = &extPtrs[0]
283		} else {
284			di.PpEnabledExtensionNames = nil
285		}
286		result := r.id.CreateDevice(physical, &di, nil, &r.device)
287		runtime.KeepAlive(global)
288		runtime.KeepAlive(coreFeatures)
289		runtime.KeepAlive(indexing)
290		runtime.KeepAlive(dynamicRendering)
291		runtime.KeepAlive(qi)
292		runtime.KeepAlive(extNames)
293		runtime.KeepAlive(extPtrs)
294		extPtrs = extPtrs[:count]
295		extNames = extNames[:count]
296		if result == vk.Success {
297			err = nil
298			break
299		}
300		err = checked("vkCreateDevice", result)
301		if attempt == 0 {
302			return
303		}
304	}
305	r.queuePriority = chosen
306	r.dd, err = vk.LoadDeviceDispatch(r.id, r.device)
307	if err != nil {
308		err = fmt.Errorf("LoadDeviceDispatch: %w", err)
309		return
310	}
311	runtime.KeepAlive(extNames)
312	runtime.KeepAlive(extPtrs)
313	if dmabufSupported {
314		// Clients need the render node to allocate on the right GPU.
315		if sup := r.probeDMABuf(physical); sup.Device != 0 {
316			r.dmabuf = sup
317			r.probeRenderModifiers(physical)
318			r.physical = physical
319		}
320		// VK_KHR_external_semaphore_fd (enabled above): frames export
321		// their fence as a sync file.
322		r.syncFD = r.dd.HasGetSemaphoreFdKHR()
323	}
324	r.dd.GetDeviceQueue(r.device, family, 0, &r.queue)
325	extent := vk.Extent3D{Width: uint32(width), Height: uint32(height), Depth: 1}
326	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}
327	if err = checked("vkCreateImage", r.dd.CreateImage(r.device, &ii, nil, &r.own.image)); err != nil {
328		return
329	}
330	var req vk.MemoryRequirements
331	r.dd.GetImageMemoryRequirements(r.device, r.own.image, &req)
332	var kind uint32
333	kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
334	if err != nil {
335		return
336	}
337	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
338	if err = checked("vkAllocateMemory(image)", r.dd.AllocateMemory(r.device, &alloc, nil, &r.own.memory)); err != nil {
339		return
340	}
341	if err = checked("vkBindImageMemory", r.dd.BindImageMemory(r.device, r.own.image, r.own.memory, 0)); err != nil {
342		return
343	}
344	pi := vk.CommandPoolCreateInfo{SType: vk.StructureTypeCommandPoolCreateInfo, Flags: vk.CommandPoolCreateResetCommandBufferBit, QueueFamilyIndex: family}
345	if err = checked("vkCreateCommandPool", r.dd.CreateCommandPool(r.device, &pi, nil, &r.pool)); err != nil {
346		return
347	}
348	if err = r.createSlots(); err != nil {
349		return
350	}
351	if err = r.createView(&r.own); err != nil {
352		return
353	}
354	if err = r.createComposer(); err != nil {
355		return
356	}
357	runtime.KeepAlive(priority)
358	return
359}
360
361func (r *Renderer) findMemoryType(bits uint32, props vk.MemoryPropertyFlags) (uint32, error) {
362	for i := uint32(0); i < r.memory.MemoryTypeCount; i++ {
363		if bits&(uint32(1)<<i) != 0 && r.memory.MemoryTypes[i].PropertyFlags&props == props {
364			return i, nil
365		}
366	}
367	return 0, fmt.Errorf("no Vulkan memory type for bits %#x and properties %#x", bits, props)
368}
369
370// CopiedBytes reports cumulative wl_shm bytes copied into GPU storage.
371// It is diagnostic only and must be read by the renderer's owner goroutine.
372func (r *Renderer) CopiedBytes() int { return r.copied }
373
374// TakeRedrawn returns the target pixels drawn since the last call and
375// resets the count.
376func (r *Renderer) TakeRedrawn() int {
377	n := r.redrawn
378	r.redrawn = 0
379	if f := r.fullRedraws.take(); f != (fullRedraws{}) {
380		r.log.Debug().Int("target", f[fullTarget]).Int("scene", f[fullScene]).Int("new_window", f[fullNewWindow]).
381			Int("empty", f[fullEmpty]).Int("children", f[fullChildren]).Int("history", f[fullHistory]).Msg("full redraws")
382	}
383	return n
384}
385
386// SetLogger sets the logger (with its component field) for render target
387// events. Not on the port; the zero logger discards.
388func (r *Renderer) SetLogger(l zerowrap.Logger) { r.log = l }
389
390// SetTraceDamage logs the redrawn pixels, draw count and partial flag of
391// every frame at debug level (--debug=render-damage). Off by default.
392func (r *Renderer) SetTraceDamage(on bool) { r.traceDamage = on }
393
394// QueuePriority is the global priority the queue got: realtime, high or
395// default.
396func (r *Renderer) QueuePriority() string { return r.queuePriority }
397
398// Pixels reads the last frame back through a capture slot, waiting for
399// the GPU on the calling goroutine (slot fence, no sync file needed):
400// headless screenshots and tests only. Nil when no slot is free (both
401// leased to captures) or the copy failed: never a silent black image.
402func (r *Renderer) Pixels() *image.RGBA {
403	cf, err := r.debugCapture()
404	if err != nil {
405		return nil
406	}
407	defer r.EndCapture(cf)
408	out := image.NewRGBA(image.Rect(0, 0, r.width, r.height))
409	if err := cf.Read(image.Rect(0, 0, r.width, r.height), out.Pix, out.Stride); err != nil {
410		return nil
411	}
412	for i := 0; i < len(out.Pix); i += 4 {
413		out.Pix[i], out.Pix[i+2] = out.Pix[i+2], out.Pix[i]
414	}
415	return out
416}
417
418// SetHDRReadback makes the exported HDR targets transfer sources so that
419// HDRPixels can read them (they are not in production: the DRM path never
420// sets it). Call before ExportTargets. Not on the port.
421func (r *Renderer) SetHDRReadback(on bool) { r.hdrReadback = on }
422
423// HDRPixels reads the last HDR frame's XRGB2101010 target back, each 10-bit
424// code scaled to 16 bits (c<<6 | c>>4), alpha opaque. It waits for the GPU:
425// debug only. Nil when the output is SDR, the target is not readable (no
426// SetHDRReadback), no HDR frame was rendered, every slot is leased or the
427// copy failed.
428func (r *Renderer) HDRPixels() *image.RGBA64 {
429	var out *image.RGBA64
430	err := r.readHDRTarget(func(words []uint32) {
431		out = image.NewRGBA64(image.Rect(0, 0, r.width, r.height))
432		for i, w := range words {
433			p := out.Pix[i*8 : i*8+8]
434			for ch, c := range [3]uint32{w >> 20 & 1023, w >> 10 & 1023, w & 1023} {
435				v := uint16(c<<6 | c>>4)
436				p[ch*2], p[ch*2+1] = byte(v>>8), byte(v)
437			}
438			p[6], p[7] = 0xff, 0xff
439		}
440	})
441	if err != nil {
442		return nil
443	}
444	return out
445}
446
447// Missing from the bindings: VK_QUEUE_FAMILY_IGNORED, VK_WHOLE_SIZE.
448const (
449	queueFamilyIgnored = ^uint32(0)
450	wholeSize          = ^vk.DeviceSize(0)
451)
452
453func (r *Renderer) Close() {
454	if r == nil {
455		return
456	}
457	if r.dd != nil {
458		d := r.dd
459		if r.device != 0 {
460			_ = checked("vkDeviceWaitIdle", d.DeviceWaitIdle(r.device))
461			r.idle()
462		}
463		r.dropTargets()
464		r.destroyHDR()
465		r.dropCursors()
466		for id, im := range r.imports {
467			r.release(im)
468			delete(r.imports, id)
469		}
470		r.frame += importTTL + 1
471		r.dropPools()
472		r.dropShm()
473		r.destroySlots()
474		r.destroyCaptures()
475		if r.pool != 0 {
476			d.DestroyCommandPool(r.device, r.pool, nil)
477			r.pool = 0
478		}
479		r.destroyComposer()
480		r.freeTarget(&r.hdrOwn)
481		r.freeTarget(&r.own)
482		d.DestroyDevice(r.device, nil)
483		r.device = 0
484	} else if r.device != 0 && r.id != nil {
485		if vk.VkDestroyDevice != nil {
486			vk.VkDestroyDevice(r.device, nil)
487		}
488		r.device = 0
489	}
490	if r.instance != 0 {
491		if r.id != nil {
492			r.id.DestroyInstance(r.instance, nil)
493		} else if vk.VkDestroyInstance != nil {
494			vk.VkDestroyInstance(r.instance, nil)
495		}
496		r.instance = 0
497	}
498}

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 23

Tags 10

↑↓ moveenter openesc close