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}