1package vulkan
2
3import (
4 "fmt"
5 "image"
6 "math"
7 "os"
8 "runtime"
9 "slices"
10 "unsafe"
11
12 "github.com/bnema/neferwl/internal/ports"
13 vk "github.com/bnema/purego-vulkan/vulkan"
14)
15
16// Frames are recorded into a ring of slots, one per frame in flight. The
17// CPU never waits for a frame it just submitted: Render returns a sync
18// file the display waits on (KMS IN_FENCE_FD). A slot's fence is waited
19// only when the slot is reused, and objects a frame read (imports, GPU
20// buffers, client semaphores) are freed only once that frame completed.
21
22// frameSlots is how many frames may be in flight: one per output target.
23const frameSlots = 2
24
25// frameSlot is the command buffer and sync objects of one frame in flight.
26type frameSlot struct {
27 cmd vk.CommandBuffer
28 fence vk.Fence
29 // done is signalled with the frame and exported as its sync file.
30 done vk.Semaphore
31 frame uint64 // the frame recorded last, 0 before any
32 busy bool // submitted, fence not yet seen signalled
33 batches []*composeBatch
34}
35
36// callScratch holds the structs Render and its record helpers pass to Vulkan
37// by pointer. The purego call makes a pointer to a local escape, so a local
38// would be a heap allocation per frame; these live in the Renderer instead
39// (owned by one goroutine, and Vulkan reads each only during its call). Every
40// field is set in full before use; two uses that overlap in time never share a
41// field.
42type callScratch struct {
43 cmd vk.CommandBuffer
44 begin vk.CommandBufferBeginInfo
45 submit vk.SubmitInfo
46 target vk.ImageMemoryBarrier // the render target's, kept to the frame's end
47 client vk.ImageMemoryBarrier // a client image's (ownership)
48 attach vk.RenderingAttachmentInfo
49 render vk.RenderingInfo
50 getFD vk.SemaphoreGetFdInfoKHR
51 fd int32
52 hdrIn vk.ImageMemoryBarrier // recordHDR
53 hdrOut vk.ImageMemoryBarrier
54 hdrNits float32
55}
56
57// deferredFree runs once frame completed. The hot path, a wait semaphore
58// of the frame, is typed (sem, with free nil) so it costs no closure; cold
59// paths set free.
60type deferredFree struct {
61 frame uint64
62 sem vk.Semaphore
63 free func()
64}
65
66// free frees what f holds.
67func (r *Renderer) free(f deferredFree) {
68 if f.free != nil {
69 f.free()
70 return
71 }
72 r.dd.DestroySemaphore(r.device, f.sem, nil)
73}
74
75func (r *Renderer) createSlots() error {
76 d := r.dd
77 cmds := make([]vk.CommandBuffer, frameSlots)
78 ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: frameSlots}
79 if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmds[0])); err != nil {
80 return err
81 }
82 for i := range r.slots {
83 s := &r.slots[i]
84 s.cmd = cmds[i]
85 fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
86 if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
87 return err
88 }
89 if err := r.newDone(s); err != nil {
90 return err
91 }
92 }
93 return nil
94}
95
96// newDone creates a slot's frame semaphore, exportable as a sync file when
97// the device allows it; a device that refuses falls back to waiting for
98// frames on the CPU (syncFD off).
99func (r *Renderer) newDone(s *frameSlot) error {
100 d := r.dd
101 si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo}
102 export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
103 if r.syncFD {
104 si.Next = unsafe.Pointer(&export)
105 if d.CreateSemaphore(r.device, &si, nil, &s.done) == vk.Success {
106 return nil
107 }
108 r.syncFD = false
109 si.Next = nil
110 }
111 return checked("vkCreateSemaphore", d.CreateSemaphore(r.device, &si, nil, &s.done))
112}
113
114func (r *Renderer) destroySlots() {
115 d := r.dd
116 for i := range r.slots {
117 s := &r.slots[i]
118 for j := range s.batches {
119 r.freeComposeBatch(s.batches[j])
120 }
121 if s.fence != 0 {
122 d.DestroyFence(r.device, s.fence, nil)
123 }
124 if s.done != 0 {
125 d.DestroySemaphore(r.device, s.done, nil)
126 }
127 if s.cmd != 0 {
128 d.FreeCommandBuffers(r.device, r.pool, 1, &s.cmd)
129 }
130 *s = frameSlot{}
131 }
132}
133
134// waitFrame blocks until frame n completed. Frames not submitted yet and
135// frames known done return at once. Every slot holding a frame up to n
136// is waited and reset, so its fence is unsignalled before reuse.
137func (r *Renderer) waitFrame(n uint64) error {
138 n = min(n, r.submitted)
139 if n <= r.completed {
140 return nil
141 }
142 for i := range r.slots {
143 s := &r.slots[i]
144 if !s.busy || s.frame > n {
145 continue
146 }
147 if err := checked("vkWaitForFences", r.dd.WaitForFences(r.device, 1, &s.fence, 1, math.MaxUint64)); err != nil {
148 return err
149 }
150 if err := checked("vkResetFences", r.dd.ResetFences(r.device, 1, &s.fence)); err != nil {
151 return err
152 }
153 s.busy = false
154 }
155 r.completed = n
156 r.runDeferred()
157 return nil
158}
159
160// retire frees an object once the frame that last used it completed.
161func (r *Renderer) retire(frame uint64, free func()) {
162 if frame <= r.completed {
163 free()
164 return
165 }
166 r.deferred = append(r.deferred, deferredFree{frame: frame, free: free})
167}
168
169// retireSemaphore destroys sem once the frame that last used it completed.
170func (r *Renderer) retireSemaphore(frame uint64, sem vk.Semaphore) {
171 if frame <= r.completed {
172 r.dd.DestroySemaphore(r.device, sem, nil)
173 return
174 }
175 r.deferred = append(r.deferred, deferredFree{frame: frame, sem: sem})
176}
177
178func (r *Renderer) runDeferred() {
179 r.deferred = slices.DeleteFunc(r.deferred, func(f deferredFree) bool {
180 if f.frame <= r.completed {
181 r.free(f)
182 return true
183 }
184 return false
185 })
186}
187
188// idle records that the device finished every submitted frame.
189func (r *Renderer) idle() {
190 for i := range r.slots {
191 s := &r.slots[i]
192 if s.busy {
193 _ = r.dd.ResetFences(r.device, 1, &s.fence)
194 s.busy = false
195 }
196 }
197 // Frames recorded but never submitted have no GPU work either.
198 r.completed = r.frame
199 r.runDeferred()
200}
201
202// Render draws a frame into the current target. done is signalled when
203// the GPU finished it; nil when the device cannot export a sync file,
204// then the frame is finished on return.
205func (r *Renderer) Render(s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) (done *os.File, err error) {
206 // A frame that fails before its submit keeps its number: objects it
207 // touched are freed once a later frame completes (never early).
208 r.frame++
209 slot := &r.slots[r.frame%frameSlots]
210 // The slot's previous frame must be done before its command buffer
211 // is recorded again.
212 if slot.busy {
213 if err := r.waitFrame(slot.frame); err != nil {
214 return nil, err
215 }
216 }
217 tg := r.target()
218 hdr := r.hdrNits > 0 && len(r.targets) > 0
219 if r.hdrNits > 0 && !hdr {
220 return nil, fmt.Errorf("HDR composition requires an exported target")
221 }
222 if hdr {
223 tg = &r.hdrOwn
224 }
225 // Bounds, damage and redrawn pixels are in scene space: the target's
226 // size, swapped for a rotated output. orient maps the draws to the target
227 // below. A transform change always comes with a new scene Seq, so a held
228 // target is never reused across transforms.
229 sw, sh := r.sceneSize(s.Transform)
230 dmg := r.frameDamage(tg, s, image.Rect(0, 0, sw, sh))
231 ds := r.draws(s, contents, dmg)
232 dmg.finish()
233 r.dropPools()
234 partial := !dmg.all()
235 redrawn := sw * sh
236 if partial {
237 redrawn = dmg.area.Dx() * dmg.area.Dy()
238 // The pass keeps the image: repaint the background under the
239 // region, then everything clipped to it.
240 // Keep the background first without allocating a second draw slice.
241 ds = append(ds, draw{})
242 copy(ds[1:], ds[:len(ds)-1])
243 ds[0] = r.fillDraw(dmg.area, parseColor(s.Background))
244 ds = dmg.clip(ds)
245 r.scratchDraws = ds
246 }
247 r.redrawn += redrawn
248 if r.traceDamage {
249 r.log.Debug().Int("redrawn", redrawn).Int("draws", len(ds)).Bool("partial", partial).Msg("frame damage")
250 }
251 r.orient(ds, s.Transform)
252 batches, err := r.prepareCompose(slot, ds)
253 if err != nil {
254 return nil, err
255 }
256 d := r.dd
257 sc := &r.call
258 cmd := slot.cmd
259 sc.cmd = cmd
260 if err := checked("vkResetCommandBuffer", d.ResetCommandBuffer(cmd, 0)); err != nil {
261 return nil, err
262 }
263 sc.begin = vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
264 if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &sc.begin)); err != nil {
265 return nil, err
266 }
267 // A full redraw clears the target: its old contents never matter
268 // (UNDEFINED). A partial one keeps them.
269 b := &sc.target
270 *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: tg.image, SubresourceRange: colorRange}
271 if partial {
272 b.OldLayout = tg.layout
273 b.DstAccessMask |= vk.AccessColorAttachmentReadBit
274 }
275 if tg.exported {
276 // Acquire from the display (foreign queue family).
277 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
278 }
279 d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit|vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, b)
280 // Client buffers come from the foreign queue family (their driver)
281 // and go back to it after the frame, so the next frame acquires them.
282 dmas := r.frameDMAs[:0]
283 if r.frameAcquires == nil {
284 r.frameAcquires = make(map[*imported]*os.File)
285 }
286 clear(r.frameAcquires)
287 acquires := r.frameAcquires
288 for _, dr := range ds {
289 if dr.im != nil && !slices.Contains(dmas, dr.im) {
290 dmas = append(dmas, dr.im)
291 dr.im.last = r.frame
292 }
293 if dr.acquire != nil {
294 acquires[dr.im] = dr.acquire
295 }
296 }
297 r.frameDMAs = dmas
298 r.ownership(cmd, dmas, true)
299 r.recordDraws(cmd, tg.view, parseColor(s.Background), batches, partial)
300 r.ownership(cmd, dmas, false)
301 if hdr {
302 r.recordHDR(cmd, r.targets[r.current])
303 }
304 if !hdr {
305 b.SrcAccessMask, b.OldLayout = vk.AccessColorAttachmentWriteBit, vk.ImageLayoutColorAttachmentOptimal
306 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = queueFamilyIgnored, queueFamilyIgnored
307 if tg.exported {
308 // Release to the display: KMS scans it out after the fence.
309 b.NewLayout, b.DstAccessMask = vk.ImageLayoutGeneral, 0
310 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
311 d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
312 } else {
313 b.NewLayout, b.DstAccessMask = vk.ImageLayoutTransferSrcOptimal, vk.AccessTransferReadBit
314 d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, b)
315 }
316 }
317 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
318 return nil, err
319 }
320 // Wait for the client's GPU writes to each buffer: its explicit
321 // acquire fence, else the buffer's implicit fences.
322 waits := r.frameWaits[:0]
323 for _, im := range dmas {
324 if f := acquires[im]; f != nil {
325 if sem := r.importFence(f); sem != 0 {
326 waits = append(waits, sem)
327 }
328 } else {
329 waits = r.appendReadFences(waits, im)
330 }
331 }
332 r.frameWaits = waits
333 frame := r.frame
334 for _, sem := range waits {
335 r.retireSemaphore(frame, sem)
336 }
337 sc.submit = vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &sc.cmd}
338 submit := &sc.submit
339 if cap(r.frameStages) < len(waits) {
340 r.frameStages = make([]vk.PipelineStageFlags, len(waits))
341 }
342 stages := r.frameStages[:len(waits)]
343 if len(waits) > 0 {
344 for i := range stages {
345 stages[i] = vk.PipelineStageFragmentShaderBit
346 }
347 submit.WaitSemaphoreCount, submit.WaitSemaphores, submit.WaitDstStageMask = uint32(len(waits)), &waits[0], &stages[0]
348 }
349 if r.syncFD {
350 submit.SignalSemaphoreCount, submit.SignalSemaphores = 1, &slot.done
351 }
352 if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, submit, slot.fence)); err != nil {
353 return nil, err
354 }
355 runtime.KeepAlive(stages)
356 runtime.KeepAlive(waits)
357 slot.frame, slot.busy, r.submitted = frame, true, frame
358 if hdr {
359 // recordHDR left the composed linear image in TRANSFER_SRC for capture.
360 tg.layout = vk.ImageLayoutTransferSrcOptimal
361 r.targets[r.current].layout = vk.ImageLayoutGeneral
362 } else {
363 tg.layout = b.NewLayout
364 }
365 oldWindows := tg.windows
366 tg.hold(s, dmg)
367 r.damageDrawn = oldWindows
368 r.last = tg
369 if hdr {
370 r.lastHDRTarget = r.current
371 }
372 r.dropUnused()
373 r.dropShm()
374 if !r.syncFD {
375 return nil, r.waitFrame(frame)
376 }
377 sc.fd = 0
378 sc.getFD = vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: slot.done, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
379 if d.GetSemaphoreFdKHR(r.device, &sc.getFD, &sc.fd) != vk.Success {
380 // The frame is drawn: finish it on the CPU. The semaphore stays
381 // signalled: replace it, so the slot's next submit signals a
382 // fresh one.
383 if err := r.waitFrame(frame); err != nil {
384 return nil, err
385 }
386 d.DestroySemaphore(r.device, slot.done, nil)
387 slot.done = 0
388 return nil, r.newDone(slot)
389 }
390 fd := sc.fd
391 if fd < 0 {
392 // Already signalled: the driver may return -1.
393 return nil, r.waitFrame(frame)
394 }
395 f := os.NewFile(uintptr(fd), "neferwl-frame")
396 if f == nil {
397 return nil, fmt.Errorf("invalid frame sync file %d", fd)
398 }
399 return f, nil
400}
401
402// ownership moves client images between their driver (foreign queue
403// family, GENERAL: their contents survive) and the fragment shader.
404func (r *Renderer) ownership(cmd vk.CommandBuffer, dmas []*imported, acquire bool) {
405 for _, im := range dmas {
406 b := &r.call.client
407 *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, Image: im.image, SubresourceRange: colorRange}
408 if im.yuv {
409 b.SubresourceRange.AspectMask = aspectPlane0 | aspectPlane1
410 }
411 if acquire {
412 b.OldLayout, b.NewLayout = vk.ImageLayoutGeneral, vk.ImageLayoutShaderReadOnlyOptimal
413 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = vk.QueueFamilyForeignEXT, r.family
414 b.DstAccessMask = vk.AccessShaderReadBit
415 // The frame waits on the client's fence at the fragment
416 // stage: the transfer must come after that wait.
417 r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, b)
418 } else {
419 b.OldLayout, b.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutGeneral
420 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
421 b.SrcAccessMask = vk.AccessShaderReadBit
422 r.dd.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, b)
423 }
424 }
425}
426
427// oneShot records and runs commands outside the frame ring and waits for
428// them (setup and screenshots only, never per frame).
429func (r *Renderer) oneShot(record func(vk.CommandBuffer)) error {
430 d := r.dd
431 var cmd vk.CommandBuffer
432 ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: 1}
433 if err := checked("vkAllocateCommandBuffers", d.AllocateCommandBuffers(r.device, &ai, &cmd)); err != nil {
434 return err
435 }
436 defer d.FreeCommandBuffers(r.device, r.pool, 1, &cmd)
437 var fence vk.Fence
438 fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
439 if err := checked("vkCreateFence", d.CreateFence(r.device, &fi, nil, &fence)); err != nil {
440 return err
441 }
442 defer d.DestroyFence(r.device, fence, nil)
443 begin := vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
444 if err := checked("vkBeginCommandBuffer", d.BeginCommandBuffer(cmd, &begin)); err != nil {
445 return err
446 }
447 record(cmd)
448 if err := checked("vkEndCommandBuffer", d.EndCommandBuffer(cmd)); err != nil {
449 return err
450 }
451 submit := vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &cmd}
452 if err := checked("vkQueueSubmit", d.QueueSubmit(r.queue, 1, &submit, fence)); err != nil {
453 return err
454 }
455 return checked("vkWaitForFences", d.WaitForFences(r.device, 1, &fence, 1, math.MaxUint64))
456}
457
458// Clear draws a background-only frame and waits for it.
459func (r *Renderer) Clear(rgb [3]uint8) error {
460 done, err := r.Render(ports.Scene{Background: fmt.Sprintf("#%02x%02x%02x", rgb[0], rgb[1], rgb[2])}, nil)
461 if done != nil {
462 done.Close()
463 }
464 if err != nil {
465 return err
466 }
467 return r.waitFrame(r.frame)
468}