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}