feat/data-dnd e05cb15

capture.go RawHistoryFolder

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, &region)
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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close