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