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