1package vulkan
2
3import (
4 _ "embed"
5 "errors"
6 "fmt"
7 "image"
8 "math"
9 "os"
10 "unsafe"
11
12 "github.com/bnema/neferwl/internal/ports"
13 vk "github.com/bnema/purego-vulkan/vulkan"
14 "golang.org/x/sys/unix"
15)
16
17//go:generate glslc -O --target-env=vulkan1.3 shaders/capture_sdr.frag -o shaders/capture_sdr.frag.spv
18
19//go:embed shaders/capture_sdr.frag.spv
20var captureFrag []byte
21
22// Asynchronous capture (screencopy). BeginCapture records a copy of the
23// last frame into one of captureSlots host-visible buffers and submits it
24// on the frame queue right after the frame, signalling a fence and a
25// semaphore exported as a sync file. A worker goroutine waits on the sync
26// file and reads the mapped buffer; the owner never waits for the GPU.
27// A slot is leased from BeginCapture to EndCapture; a returned slot is
28// reused only once its fence polls signalled, so an early return (worker
29// cancelled) never lets a running copy be recorded over.
30//
31// HDR frames are converted on the GPU by the capture pass (capture_sdr
32// shader) into a reusable B8G8R8A8 image before the copy.
33
34// captureSlots is how many captures may be leased at once.
35const captureSlots = 2
36
37// captureMaxBytes caps the host memory of one renderer's capture slots
38// (captureSlots × width × height × 4): 128 MiB holds two 5K slots;
39// larger outputs refuse captures.
40const captureMaxBytes = 128 << 20
41
42// captureBytesPerPixel is the size of one sRGB8 BGRA capture pixel.
43const captureBytesPerPixel = 4
44
45// captureCurve is the tone map of the capture pass on the largest
46// component, mirrored from capture_sdr.frag: identity to the knee, then
47// a rational shoulder (scale captureScale) that never reaches 1.
48const (
49 captureKnee = 0.9
50 captureScale = 0.5
51)
52
53func captureCurve(m float64) float64 {
54 if m <= captureKnee {
55 return m
56 }
57 t := (m - captureKnee) / captureScale
58 return captureKnee + (1-captureKnee)*t/(1+t)
59}
60
61// captureSDR maps one linear BT.709 pixel in SDR-white units to the sRGB8
62// bytes the capture pass produces (tests compare against it).
63func captureSDR(c [3]float64) [3]uint8 {
64 for i, v := range c {
65 if math.IsNaN(v) {
66 c[i] = 0
67 }
68 c[i] = math.Max(-65504, math.Min(65504, c[i]))
69 }
70 y := math.Max(0.2126*c[0]+0.7152*c[1]+0.0722*c[2], 0)
71 lo := math.Min(math.Min(c[0], c[1]), c[2])
72 if lo < 0 {
73 t := math.Min(1, -lo/math.Max(y-lo, 1e-6))
74 for i := range c {
75 c[i] += t * (y - c[i])
76 }
77 }
78 m := 0.0
79 for i := range c {
80 c[i] = math.Max(c[i], 0)
81 m = math.Max(m, c[i])
82 }
83 if m > captureKnee {
84 s := captureCurve(m) / m
85 for i := range c {
86 c[i] *= s
87 }
88 }
89 var out [3]uint8
90 for i, v := range c {
91 out[i] = encodeSRGB8(float32(v))
92 }
93 return out
94}
95
96// encodeSRGB8 clips a linear-light value to [0, 1] and encodes it as sRGB.
97func encodeSRGB8(f float32) byte {
98 v := float64(f)
99 if !(v > 0) { // also maps NaN to black
100 return 0
101 }
102 v = math.Min(v, 1)
103 if v <= .0031308 {
104 v *= 12.92
105 } else {
106 v = 1.055*math.Pow(v, 1/2.4) - .055
107 }
108 return byte(math.Round(v * 255))
109}
110
111// captureScratch holds the structs a capture passes to Vulkan by pointer.
112// The purego call makes a pointer to a local escape, so a local would be a
113// heap allocation per capture; these live in the Renderer instead (the
114// owner goroutine uses them, and Vulkan reads each only during its call).
115// Every field is set in full before use. Sequential uses may share a field;
116// the sync export uses Renderer.call.getFD and .fd.
117type captureScratch struct {
118 cmd vk.CommandBuffer
119 fence vk.Fence
120 begin vk.CommandBufferBeginInfo
121 submit vk.SubmitInfo
122 // The copy (recordCaptureCopy).
123 region vk.BufferImageCopy
124 hb vk.BufferMemoryBarrier
125 b vk.ImageMemoryBarrier
126 // The HDR conversion pass (recordCaptureHDR).
127 hdrIn, hdrB vk.ImageMemoryBarrier
128 attachment vk.RenderingAttachmentInfo
129 info vk.RenderingInfo
130}
131
132// captureSync is the fence and semaphore surface of a capture slot: the
133// non-blocking poll, the sync file export and the semaphore replacement
134// (a seam for tests; the device implements it).
135type captureSync interface {
136 // fenceStatus reports Success (signalled), NotReady/Timeout (still
137 // running) or an error, without waiting.
138 fenceStatus(f vk.Fence) vk.Result
139 resetFence(f vk.Fence) vk.Result
140 // exportSyncFD exports the semaphore's pending signal as a sync file
141 // (-1: already signalled).
142 exportSyncFD(s vk.Semaphore) (int32, vk.Result)
143 newSemaphore() (vk.Semaphore, error)
144 destroySemaphore(s vk.Semaphore)
145}
146
147// deviceSync is the captureSync of the renderer's device.
148type deviceSync struct{ r *Renderer }
149
150func (d deviceSync) fenceStatus(f vk.Fence) vk.Result {
151 // A zero timeout only reports the fence state (the bindings lack
152 // vkGetFenceStatus): Timeout while the copy runs.
153 d.r.capCall.fence = f
154 return d.r.dd.WaitForFences(d.r.device, 1, &d.r.capCall.fence, 1, 0)
155}
156
157func (d deviceSync) resetFence(f vk.Fence) vk.Result {
158 d.r.capCall.fence = f
159 return d.r.dd.ResetFences(d.r.device, 1, &d.r.capCall.fence)
160}
161
162func (d deviceSync) exportSyncFD(s vk.Semaphore) (int32, vk.Result) {
163 sc := &d.r.call
164 sc.fd = -1
165 sc.getFD = vk.SemaphoreGetFdInfoKHR{SType: vk.StructureTypeSemaphoreGetFDInfoKHR, Semaphore: s, HandleType: vk.ExternalSemaphoreHandleTypeSyncFDBit}
166 res := d.r.dd.GetSemaphoreFdKHR(d.r.device, &sc.getFD, &sc.fd)
167 return sc.fd, res
168}
169
170func (d deviceSync) newSemaphore() (vk.Semaphore, error) {
171 export := vk.ExportSemaphoreCreateInfo{SType: vk.StructureTypeExportSemaphoreCreateInfo, HandleTypes: vk.ExternalSemaphoreHandleTypeSyncFDBit}
172 si := vk.SemaphoreCreateInfo{SType: vk.StructureTypeSemaphoreCreateInfo, Next: unsafe.Pointer(&export)}
173 var s vk.Semaphore
174 err := checked("vkCreateSemaphore(capture)", d.r.dd.CreateSemaphore(d.r.device, &si, nil, &s))
175 return s, err
176}
177
178func (d deviceSync) destroySemaphore(s vk.Semaphore) { d.r.dd.DestroySemaphore(d.r.device, s, nil) }
179
180// captureSlot is one readback buffer with its copy command and sync.
181type captureSlot struct {
182 buffer vk.Buffer
183 memory vk.DeviceMemory
184 mapped unsafe.Pointer
185 cmd vk.CommandBuffer
186 fence vk.Fence // signalled by the copy; polled before reuse
187 done vk.Semaphore // signalled by the copy; exported as the sync file
188 // leased: between BeginCapture and EndCapture. pending: submitted,
189 // fence not yet seen signalled (set with leased, cleared by a poll).
190 leased, pending bool
191 // replaceDone: the last export failed, so done may stay signalled;
192 // it is replaced once the fence polls signalled, before reuse.
193 replaceDone bool
194 // frame is the slot's lease object, reused for every lease: a
195 // handle kept after EndCapture is invalid by contract (its next
196 // state belongs to another lease).
197 frame captureFrame
198 // file wraps syncFD, the slot's sync file descriptor, which the slot
199 // owns from the first export to freeCaptureSlot. Every later export
200 // is moved onto the same descriptor number (dup3), so a capture
201 // makes no os.File and the descriptor is closed exactly once. Nil
202 // until the first export.
203 file *os.File
204 syncFD int
205}
206
207// adoptSync makes fd, a sync file the caller owns, the slot's. It always
208// takes ownership of fd: fd is closed on failure and after the move. The
209// slot's previous sync file is replaced, so a lease must have ended first.
210func (s *captureSlot) adoptSync(fd int) error {
211 if s.file == nil {
212 f := os.NewFile(uintptr(fd), "neferwl-capture")
213 if f == nil {
214 return fmt.Errorf("invalid capture sync file %d", fd)
215 }
216 s.file, s.syncFD = f, fd
217 return nil
218 }
219 err := unix.Dup3(fd, s.syncFD, unix.O_CLOEXEC)
220 _ = unix.Close(fd)
221 if err != nil {
222 return fmt.Errorf("move capture sync file: %w", err)
223 }
224 return nil
225}
226
227// captureFrame is the lease handed to the worker (ports.CaptureFrame).
228// Its fields are written by the owner before the handoff and read by the
229// worker; the owner touches them again only after EndCapture.
230type captureFrame struct {
231 r *Renderer
232 slot *captureSlot
233 // done is the slot's sync file (nil: nothing to wait for). The slot
234 // owns it: valid until EndCapture, never closed by the reader.
235 done *os.File
236 pixels []byte // the slot's mapping, width*height*4
237 w, h int
238 valid bool // false after EndCapture
239}
240
241func (f *captureFrame) Done() *os.File { return f.done }
242
243// Read copies region from the mapped slot as opaque BGRA rows.
244func (f *captureFrame) Read(region image.Rectangle, dst []byte, stride int) error {
245 if !f.valid {
246 return errors.New("capture frame ended")
247 }
248 w, h := region.Dx(), region.Dy()
249 need := w * captureBytesPerPixel
250 if region.Empty() || !region.In(image.Rect(0, 0, f.w, f.h)) || stride < need || len(dst) < need {
251 return errors.New("invalid capture region or destination")
252 }
253 // The last row must fit: (h-1)*stride + need <= len(dst), checked by
254 // division so a huge stride cannot overflow the product.
255 if h > 1 && stride > (len(dst)-need)/(h-1) {
256 return errors.New("capture destination too small for stride")
257 }
258 src := f.pixels
259 for y := range h {
260 start := ((region.Min.Y+y)*f.w + region.Min.X) * captureBytesPerPixel
261 row := dst[y*stride : y*stride+w*captureBytesPerPixel]
262 copy(row, src[start:start+w*captureBytesPerPixel])
263 // The output is opaque: x-format client buffers leave alpha undefined.
264 for x := 3; x < len(row); x += captureBytesPerPixel {
265 row[x] = 255
266 }
267 }
268 return nil
269}
270
271// captureSize is the bytes of one slot, or an error over the cap.
272func (r *Renderer) captureSize() (vk.DeviceSize, error) {
273 size := uint64(r.width) * uint64(r.height) * captureBytesPerPixel
274 if size == 0 || size > math.MaxInt || size*captureSlots > captureMaxBytes {
275 return 0, fmt.Errorf("capture of %dx%d exceeds %d bytes", r.width, r.height, captureMaxBytes)
276 }
277 return vk.DeviceSize(size), nil
278}
279
280// createCaptureSlots allocates every slot on the first accepted capture.
281func (r *Renderer) createCaptureSlots() (err error) {
282 if r.captures != nil {
283 return nil
284 }
285 size, err := r.captureSize()
286 if err != nil {
287 return err
288 }
289 d := r.dd
290 cmds := make([]vk.CommandBuffer, captureSlots)
291 ai := vk.CommandBufferAllocateInfo{SType: vk.StructureTypeCommandBufferAllocateInfo, CommandPool: r.pool, Level: vk.CommandBufferLevelPrimary, CommandBufferCount: captureSlots}
292 if err := checked("vkAllocateCommandBuffers(capture)", d.AllocateCommandBuffers(r.device, &ai, &cmds[0])); err != nil {
293 return err
294 }
295 // Every command buffer belongs to a slot before anything can fail,
296 // so a failure frees them all.
297 slots := make([]*captureSlot, captureSlots)
298 for i := range slots {
299 slots[i] = &captureSlot{cmd: cmds[i]}
300 }
301 defer func() {
302 if err != nil {
303 for _, s := range slots {
304 r.freeCaptureSlot(s)
305 }
306 }
307 }()
308 for _, s := range slots {
309 bi := vk.BufferCreateInfo{SType: vk.StructureTypeBufferCreateInfo, Size: size, Usage: vk.BufferUsageTransferDstBit, SharingMode: vk.SharingModeExclusive}
310 if err := checked("vkCreateBuffer(capture)", d.CreateBuffer(r.device, &bi, nil, &s.buffer)); err != nil {
311 return err
312 }
313 var req vk.MemoryRequirements
314 d.GetBufferMemoryRequirements(r.device, s.buffer, &req)
315 // The worker reads the whole buffer: prefer cached host memory.
316 kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit|vk.MemoryPropertyHostCachedBit)
317 if err != nil {
318 kind, err = r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyHostVisibleBit|vk.MemoryPropertyHostCoherentBit)
319 }
320 if err != nil {
321 return err
322 }
323 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
324 if err := checked("vkAllocateMemory(capture)", d.AllocateMemory(r.device, &alloc, nil, &s.memory)); err != nil {
325 return err
326 }
327 if err := checked("vkBindBufferMemory(capture)", d.BindBufferMemory(r.device, s.buffer, s.memory, 0)); err != nil {
328 return err
329 }
330 if err := checked("vkMapMemory(capture)", d.MapMemory(r.device, s.memory, 0, size, 0, &s.mapped)); err != nil {
331 return err
332 }
333 fi := vk.FenceCreateInfo{SType: vk.StructureTypeFenceCreateInfo}
334 if err := checked("vkCreateFence(capture)", d.CreateFence(r.device, &fi, nil, &s.fence)); err != nil {
335 return err
336 }
337 if s.done, err = r.captureSync().newSemaphore(); err != nil {
338 return err
339 }
340 s.frame = captureFrame{r: r, slot: s, w: r.width, h: r.height, pixels: unsafe.Slice((*byte)(s.mapped), int(size))}
341 }
342 r.captures = slots
343 return nil
344}
345
346// captureSync is the slot sync seam: the device unless a test replaced it.
347func (r *Renderer) captureSync() captureSync {
348 if r.sync == nil {
349 r.sync = deviceSync{r}
350 }
351 return r.sync
352}
353
354// freeCaptureSlot destroys a slot; the device must be idle for it.
355func (r *Renderer) freeCaptureSlot(s *captureSlot) {
356 d := r.dd
357 s.frame.valid = false
358 s.frame.done = nil
359 if s.file != nil {
360 _ = s.file.Close()
361 }
362 if s.done != 0 {
363 r.captureSync().destroySemaphore(s.done)
364 }
365 if s.fence != 0 {
366 d.DestroyFence(r.device, s.fence, nil)
367 }
368 if s.mapped != nil {
369 d.UnmapMemory(r.device, s.memory)
370 }
371 if s.buffer != 0 {
372 d.DestroyBuffer(r.device, s.buffer, nil)
373 }
374 if s.memory != 0 {
375 d.FreeMemory(r.device, s.memory, nil)
376 }
377 if s.cmd != 0 {
378 d.FreeCommandBuffers(r.device, r.pool, 1, &s.cmd)
379 }
380 *s = captureSlot{}
381}
382
383// destroyCaptures frees the slots and the HDR conversion pass. Every
384// lease is invalid afterwards; the device is idle (Close).
385func (r *Renderer) destroyCaptures() {
386 for _, s := range r.captures {
387 r.freeCaptureSlot(s)
388 }
389 r.captures = nil
390 r.destroyCapturePass()
391 r.freeTarget(&r.captureSDR)
392}
393
394// freeCapture picks a slot: unleased and, if a copy was submitted, its
395// fence signalled (polled, never waited). Nil when none is free; an
396// error when the device fails to report or reset a fence.
397func (r *Renderer) freeCapture() (*captureSlot, error) {
398 sync := r.captureSync()
399 for _, s := range r.captures {
400 if s.leased {
401 continue
402 }
403 if s.pending {
404 switch res := sync.fenceStatus(s.fence); res {
405 case vk.Success:
406 case vk.Timeout, vk.NotReady:
407 continue
408 default:
409 return nil, checked("vkWaitForFences(capture poll)", res)
410 }
411 if err := checked("vkResetFences(capture)", sync.resetFence(s.fence)); err != nil {
412 return nil, err
413 }
414 s.pending = false
415 }
416 if s.replaceDone {
417 // The copy is done. Retry a failed replacement before reuse,
418 // even if its fence was already reset on the previous attempt.
419 if s.done != 0 {
420 sync.destroySemaphore(s.done)
421 s.done = 0
422 }
423 done, err := sync.newSemaphore()
424 if err != nil {
425 return nil, err
426 }
427 s.done, s.replaceDone = done, false
428 }
429 return s, nil
430 }
431 return nil, nil
432}
433
434// CaptureSupported reports whether BeginCapture can work on this device: it
435// needs sync-file export, which software devices such as lavapipe lack.
436func (r *Renderer) CaptureSupported() bool {
437 return r.dd != nil && r.syncFD && r.dd.HasGetSemaphoreFdKHR()
438}
439
440// BeginCapture submits a GPU copy of the last frame and leases its slot.
441// The lease is valid until EndCapture and no longer: the same object
442// serves the slot's next lease.
443func (r *Renderer) BeginCapture() (ports.CaptureFrame, error) {
444 f, err := r.beginCapture(false)
445 if err != nil {
446 return nil, err
447 }
448 return f, nil
449}
450
451// beginCapture is BeginCapture; debug (Pixels) skips the sync file, the
452// caller waits on the slot's fence instead.
453func (r *Renderer) beginCapture(debug bool) (*captureFrame, error) {
454 if r.device == 0 || r.dd == nil {
455 return nil, errors.New("renderer closed")
456 }
457 if r.last == nil || r.last.image == 0 {
458 return nil, errors.New("no rendered frame")
459 }
460 if !debug && (!r.syncFD || !r.dd.HasGetSemaphoreFdKHR()) {
461 return nil, errors.New("capture needs sync file export")
462 }
463 if r.submitted != r.frame {
464 return nil, errors.New("last frame not submitted")
465 }
466 if err := r.createCaptureSlots(); err != nil {
467 return nil, err
468 }
469 hdr := r.last == &r.hdrOwn
470 if hdr {
471 if err := r.createCapturePass(); err != nil {
472 return nil, err
473 }
474 }
475 s, err := r.freeCapture()
476 if err != nil {
477 return nil, err
478 }
479 if s == nil {
480 return nil, ports.ErrCaptureBusy
481 }
482 d, sc := r.dd, &r.capCall
483 sc.cmd = s.cmd
484 cmd := sc.cmd
485 if err := checked("vkResetCommandBuffer(capture)", d.ResetCommandBuffer(cmd, 0)); err != nil {
486 return nil, err
487 }
488 sc.begin = vk.CommandBufferBeginInfo{SType: vk.StructureTypeCommandBufferBeginInfo, Flags: vk.CommandBufferUsageOneTimeSubmitBit}
489 if err := checked("vkBeginCommandBuffer(capture)", d.BeginCommandBuffer(cmd, &sc.begin)); err != nil {
490 return nil, err
491 }
492 if hdr {
493 r.recordCaptureHDR(cmd, s)
494 } else {
495 r.recordCaptureCopy(cmd, r.last, r.last.layout, s)
496 }
497 if err := checked("vkEndCommandBuffer(capture)", d.EndCommandBuffer(cmd)); err != nil {
498 return nil, err
499 }
500 // After the frame on the same queue: the copy reads what it drew, and
501 // the next frame's first barrier (transfer source stage) orders its
502 // writes after this read.
503 sc.submit = vk.SubmitInfo{SType: vk.StructureTypeSubmitInfo, CommandBufferCount: 1, CommandBuffers: &sc.cmd}
504 if !debug {
505 // The export below takes the signal; a debug capture waits on
506 // the fence only, so the semaphore must not be signalled (a
507 // binary semaphore is signalled once, then waited or exported).
508 sc.submit.SignalSemaphoreCount, sc.submit.SignalSemaphores = 1, &s.done
509 }
510 if err := checked("vkQueueSubmit(capture)", d.QueueSubmit(r.queue, 1, &sc.submit, s.fence)); err != nil {
511 return nil, err
512 }
513 s.pending = true
514 f := &s.frame
515 f.done = nil
516 if !debug {
517 fd, res := r.captureSync().exportSyncFD(s.done)
518 if res != vk.Success {
519 // The copy runs untracked by any sync file: the slot stays
520 // pending until its fence polls signalled, and only then is
521 // the semaphore (still to be signalled) replaced.
522 s.replaceDone = true
523 return nil, checked("vkGetSemaphoreFdKHR(capture)", res)
524 }
525 if fd >= 0 {
526 // -1: already signalled at export, nothing to wait for.
527 if err := s.adoptSync(int(fd)); err != nil {
528 return nil, err
529 }
530 f.done = s.file
531 }
532 }
533 s.leased, f.valid = true, true
534 return f, nil
535}
536
537// EndCapture returns a lease. The slot's copy may still run: it stays
538// pending until a later BeginCapture polls its fence.
539func (r *Renderer) EndCapture(cf ports.CaptureFrame) {
540 f, ok := cf.(*captureFrame)
541 if !ok || f == nil || f.r != r || !f.valid {
542 return
543 }
544 f.valid = false
545 f.done = nil // the slot keeps the descriptor for its next lease
546 f.slot.leased = false
547}
548
549// debugCapture leases a capture and waits for it on the CPU (slot fence):
550// Pixels only, never the protocol path. It does not take leased slots.
551func (r *Renderer) debugCapture() (*captureFrame, error) {
552 f, err := r.beginCapture(true)
553 if err != nil {
554 return nil, err
555 }
556 if err := checked("vkWaitForFences(capture)", r.dd.WaitForFences(r.device, 1, &f.slot.fence, 1, math.MaxUint64)); err != nil {
557 r.EndCapture(f)
558 return nil, err
559 }
560 return f, nil
561}
562
563// readHDRTarget copies the exported PQ target last rendered (XRGB2101010
564// words, one per pixel) through a free capture slot and waits for the GPU.
565// Exported targets are transfer sources only when hdrReadback is set; it
566// errors when there is no HDR frame to read, every slot is leased or the
567// copy fails. read gets the words in the slot's mapped memory, valid only
568// during the call: copy what must outlive it.
569func (r *Renderer) readHDRTarget(read func(words []uint32)) error {
570 switch {
571 case r.device == 0 || r.dd == nil:
572 return errors.New("renderer closed")
573 case r.hdrNits == 0 || !r.hdrReadback:
574 return errors.New("HDR readback not enabled")
575 case r.lastHDRTarget < 0 || r.lastHDRTarget >= len(r.targets):
576 return errors.New("no exported HDR target")
577 case r.last != &r.hdrOwn:
578 return errors.New("no HDR frame rendered")
579 case r.submitted != r.frame:
580 return errors.New("last frame not submitted")
581 }
582 if err := r.waitFrame(r.submitted); err != nil {
583 return err
584 }
585 if err := r.createCaptureSlots(); err != nil {
586 return err
587 }
588 slot, err := r.freeCapture()
589 if err != nil {
590 return err
591 }
592 if slot == nil {
593 return ports.ErrCaptureBusy
594 }
595 target := r.targets[r.lastHDRTarget]
596 err = r.oneShot(func(cmd vk.CommandBuffer) {
597 d := r.dd
598 // Acquire from the display, copy, release back.
599 b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessTransferReadBit, OldLayout: vk.ImageLayoutGeneral, NewLayout: vk.ImageLayoutTransferSrcOptimal, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: target.image, SubresourceRange: colorRange}
600 d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
601 region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
602 d.CmdCopyImageToBuffer(cmd, target.image, vk.ImageLayoutTransferSrcOptimal, slot.buffer, 1, ®ion)
603 hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: slot.buffer, Size: wholeSize}
604 d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
605 b.SrcAccessMask, b.DstAccessMask = vk.AccessTransferReadBit, 0
606 b.OldLayout, b.NewLayout = vk.ImageLayoutTransferSrcOptimal, vk.ImageLayoutGeneral
607 b.SrcQueueFamilyIndex, b.DstQueueFamilyIndex = r.family, vk.QueueFamilyForeignEXT
608 d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
609 })
610 if err != nil {
611 return err
612 }
613 read(unsafe.Slice((*uint32)(slot.mapped), r.width*r.height))
614 return nil
615}
616
617// recordCaptureCopy copies image t (in layout) into the slot's buffer.
618// Exported targets are borrowed from the display without a layout
619// change: the display may be reading them at the same time.
620func (r *Renderer) recordCaptureCopy(cmd vk.CommandBuffer, t *target, layout vk.ImageLayout, s *captureSlot) {
621 d, sc := r.dd, &r.capCall
622 if t.exported {
623 // Acquire from the display. The frame released the image at
624 // its color attachment stage: this acquire waits for that.
625 sc.b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessTransferReadBit, OldLayout: layout, NewLayout: layout, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: t.image, SubresourceRange: colorRange}
626 d.CmdPipelineBarrier(cmd, vk.PipelineStageAllCommandsBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &sc.b)
627 }
628 sc.region = vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
629 d.CmdCopyImageToBuffer(cmd, t.image, layout, s.buffer, 1, &sc.region)
630 sc.hb = vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: s.buffer, Size: wholeSize}
631 d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &sc.hb, 0, nil)
632 if t.exported {
633 sc.b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferReadBit, OldLayout: layout, NewLayout: layout, SrcQueueFamilyIndex: r.family, DstQueueFamilyIndex: vk.QueueFamilyForeignEXT, Image: t.image, SubresourceRange: colorRange}
634 d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &sc.b)
635 }
636}
637
638// recordCaptureHDR converts hdrOwn (TRANSFER_SRC after an HDR frame) into
639// the sRGB8 capture image with the capture pass, then copies that. hdrOwn
640// is put back in TRANSFER_SRC, as the next frame expects.
641func (r *Renderer) recordCaptureHDR(cmd vk.CommandBuffer, s *captureSlot) {
642 d, p, out, sc := r.dd, &r.capturePass, &r.captureSDR, &r.capCall
643 in, b := &sc.hdrIn, &sc.hdrB
644 *in = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferReadBit, DstAccessMask: vk.AccessShaderReadBit, OldLayout: vk.ImageLayoutTransferSrcOptimal, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: r.hdrOwn.image, SubresourceRange: colorRange}
645 d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, in)
646 // The previous capture read the image as a transfer source.
647 *b = vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferReadBit, DstAccessMask: vk.AccessColorAttachmentWriteBit, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutColorAttachmentOptimal, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: out.image, SubresourceRange: colorRange}
648 d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, b)
649 sc.attachment = vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}
650 sc.info = vk.RenderingInfo{SType: vk.StructureTypeRenderingInfo, RenderArea: vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}, LayerCount: 1, ColorAttachmentCount: 1, ColorAttachments: &sc.attachment}
651 d.CmdBeginRendering(cmd, &sc.info)
652 d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, p.pipeline)
653 d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, p.layout, 0, 1, &p.set, 0, nil)
654 d.CmdDraw(cmd, 3, 1, 0, 0)
655 d.CmdEndRendering(cmd)
656 in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit
657 in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal
658 d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, in)
659 b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferReadBit
660 b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferSrcOptimal
661 d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, b)
662 r.recordCaptureCopy(cmd, out, vk.ImageLayoutTransferSrcOptimal, s)
663}
664
665// createCapturePass makes the sRGB8 image and the conversion pipeline
666// once, on the first HDR capture.
667func (r *Renderer) createCapturePass() (err error) {
668 if r.capturePass.pipeline != 0 {
669 return nil
670 }
671 if r.hdrOwn.view == 0 {
672 return errors.New("no HDR composition image")
673 }
674 defer func() {
675 if err != nil {
676 r.destroyCapturePass()
677 r.freeTarget(&r.captureSDR)
678 }
679 }()
680 d, p, t := r.dd, &r.capturePass, &r.captureSDR
681 if t.image == 0 {
682 ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: targetUsage, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
683 if err := checked("vkCreateImage(capture)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
684 return err
685 }
686 var req vk.MemoryRequirements
687 d.GetImageMemoryRequirements(r.device, t.image, &req)
688 kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
689 if err != nil {
690 return err
691 }
692 alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
693 if err := checked("vkAllocateMemory(capture)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
694 return err
695 }
696 if err := checked("vkBindImageMemory(capture)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
697 return err
698 }
699 if err := r.createView(t); err != nil {
700 return err
701 }
702 }
703 binding := vk.DescriptorSetLayoutBinding{Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}
704 sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &binding}
705 if err := checked("vkCreateDescriptorSetLayout(capture)", d.CreateDescriptorSetLayout(r.device, &sli, nil, &p.setLayout)); err != nil {
706 return err
707 }
708 pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &p.setLayout}
709 if err := checked("vkCreatePipelineLayout(capture)", d.CreatePipelineLayout(r.device, &pli, nil, &p.layout)); err != nil {
710 return err
711 }
712 si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterNearest, MinFilter: vk.FilterNearest, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
713 if err := checked("vkCreateSampler(capture)", d.CreateSampler(r.device, &si, nil, &p.sampler)); err != nil {
714 return err
715 }
716 size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}
717 dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
718 if err := checked("vkCreateDescriptorPool(capture)", d.CreateDescriptorPool(r.device, &dpi, nil, &p.pool)); err != nil {
719 return err
720 }
721 dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: p.pool, DescriptorSetCount: 1, SetLayouts: &p.setLayout}
722 if err := checked("vkAllocateDescriptorSets(capture)", d.AllocateDescriptorSets(r.device, &dai, &p.set)); err != nil {
723 return err
724 }
725 img := vk.DescriptorImageInfo{Sampler: p.sampler, ImageView: r.hdrOwn.view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
726 write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: p.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}
727 d.UpdateDescriptorSets(r.device, 1, &write, 0, nil)
728 // The pipeline is last: it marks the pass complete.
729 return r.createGraphicsPipeline(hdrVert, captureFrag, vk.FormatB8g8r8a8Unorm, p.layout, false, nil, &p.pipeline)
730}
731
732// destroyCapturePass frees the conversion pipeline (device idle).
733func (r *Renderer) destroyCapturePass() {
734 if r.dd == nil {
735 return
736 }
737 d, p := r.dd, &r.capturePass
738 if p.pool != 0 {
739 d.DestroyDescriptorPool(r.device, p.pool, nil)
740 }
741 if p.sampler != 0 {
742 d.DestroySampler(r.device, p.sampler, nil)
743 }
744 if p.pipeline != 0 {
745 d.DestroyPipeline(r.device, p.pipeline, nil)
746 }
747 if p.layout != 0 {
748 d.DestroyPipelineLayout(r.device, p.layout, nil)
749 }
750 if p.setLayout != 0 {
751 d.DestroyDescriptorSetLayout(r.device, p.setLayout, nil)
752 }
753 *p = hdrPass{}
754}