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