v0.2.0 13c6a90

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)
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// recordCaptureCopy copies image t (in layout) into the slot's buffer.
512// Exported targets are borrowed from the display without a layout
513// change: the display may be reading them at the same time.
514func (r *Renderer) recordCaptureCopy(cmd vk.CommandBuffer, t *target, layout vk.ImageLayout, s *captureSlot) {
515	d := r.dd
516	if t.exported {
517		// Acquire from the display. The frame released the image at
518		// its color attachment stage: this acquire waits for that.
519		b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, DstAccessMask: vk.AccessTransferReadBit, OldLayout: layout, NewLayout: layout, SrcQueueFamilyIndex: vk.QueueFamilyForeignEXT, DstQueueFamilyIndex: r.family, Image: t.image, SubresourceRange: colorRange}
520		d.CmdPipelineBarrier(cmd, vk.PipelineStageAllCommandsBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
521	}
522	region := vk.BufferImageCopy{ImageSubresource: vk.ImageSubresourceLayers{AspectMask: vk.ImageAspectColorBit, LayerCount: 1}, ImageExtent: vk.Extent3D{Width: uint32(r.width), Height: uint32(r.height), Depth: 1}}
523	d.CmdCopyImageToBuffer(cmd, t.image, layout, s.buffer, 1, &region)
524	hb := vk.BufferMemoryBarrier{SType: vk.StructureTypeBufferMemoryBarrier, SrcAccessMask: vk.AccessTransferWriteBit, DstAccessMask: vk.AccessHostReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Buffer: s.buffer, Size: wholeSize}
525	d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageHostBit, 0, 0, nil, 1, &hb, 0, nil)
526	if t.exported {
527		b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, SrcAccessMask: vk.AccessTransferReadBit, OldLayout: layout, NewLayout: layout, SrcQueueFamilyIndex: r.family, DstQueueFamilyIndex: vk.QueueFamilyForeignEXT, Image: t.image, SubresourceRange: colorRange}
528		d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageBottomOfPipeBit, 0, 0, nil, 0, nil, 1, &b)
529	}
530}
531
532// recordCaptureHDR converts hdrOwn (TRANSFER_SRC after an HDR frame) into
533// the sRGB8 capture image with the capture pass, then copies that. hdrOwn
534// is put back in TRANSFER_SRC, as the next frame expects.
535func (r *Renderer) recordCaptureHDR(cmd vk.CommandBuffer, s *captureSlot) {
536	d, p, out := r.dd, &r.capturePass, &r.captureSDR
537	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}
538	d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &in)
539	// The previous capture read the image as a transfer source.
540	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}
541	d.CmdPipelineBarrier(cmd, vk.PipelineStageTransferBit, vk.PipelineStageColorAttachmentOutputBit, 0, 0, nil, 0, nil, 1, &b)
542	attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: out.view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpDontCare, StoreOp: vk.AttachmentStoreOpStore}
543	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}
544	d.CmdBeginRendering(cmd, &info)
545	d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, p.pipeline)
546	d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, p.layout, 0, 1, &p.set, 0, nil)
547	d.CmdDraw(cmd, 3, 1, 0, 0)
548	d.CmdEndRendering(cmd)
549	in.SrcAccessMask, in.DstAccessMask = vk.AccessShaderReadBit, vk.AccessTransferReadBit
550	in.OldLayout, in.NewLayout = vk.ImageLayoutShaderReadOnlyOptimal, vk.ImageLayoutTransferSrcOptimal
551	d.CmdPipelineBarrier(cmd, vk.PipelineStageFragmentShaderBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &in)
552	b.SrcAccessMask, b.DstAccessMask = vk.AccessColorAttachmentWriteBit, vk.AccessTransferReadBit
553	b.OldLayout, b.NewLayout = vk.ImageLayoutColorAttachmentOptimal, vk.ImageLayoutTransferSrcOptimal
554	d.CmdPipelineBarrier(cmd, vk.PipelineStageColorAttachmentOutputBit, vk.PipelineStageTransferBit, 0, 0, nil, 0, nil, 1, &b)
555	r.recordCaptureCopy(cmd, out, vk.ImageLayoutTransferSrcOptimal, s)
556}
557
558// createCapturePass makes the sRGB8 image and the conversion pipeline
559// once, on the first HDR capture.
560func (r *Renderer) createCapturePass() (err error) {
561	if r.capturePass.pipeline != 0 {
562		return nil
563	}
564	if r.hdrOwn.view == 0 {
565		return errors.New("no HDR composition image")
566	}
567	defer func() {
568		if err != nil {
569			r.destroyCapturePass()
570			r.freeTarget(&r.captureSDR)
571		}
572	}()
573	d, p, t := r.dd, &r.capturePass, &r.captureSDR
574	if t.image == 0 {
575		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}
576		if err := checked("vkCreateImage(capture)", d.CreateImage(r.device, &ii, nil, &t.image)); err != nil {
577			return err
578		}
579		var req vk.MemoryRequirements
580		d.GetImageMemoryRequirements(r.device, t.image, &req)
581		kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
582		if err != nil {
583			return err
584		}
585		alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
586		if err := checked("vkAllocateMemory(capture)", d.AllocateMemory(r.device, &alloc, nil, &t.memory)); err != nil {
587			return err
588		}
589		if err := checked("vkBindImageMemory(capture)", d.BindImageMemory(r.device, t.image, t.memory, 0)); err != nil {
590			return err
591		}
592		if err := r.createView(t); err != nil {
593			return err
594		}
595	}
596	binding := vk.DescriptorSetLayoutBinding{Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit}
597	sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: 1, Bindings: &binding}
598	if err := checked("vkCreateDescriptorSetLayout(capture)", d.CreateDescriptorSetLayout(r.device, &sli, nil, &p.setLayout)); err != nil {
599		return err
600	}
601	pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &p.setLayout}
602	if err := checked("vkCreatePipelineLayout(capture)", d.CreatePipelineLayout(r.device, &pli, nil, &p.layout)); err != nil {
603		return err
604	}
605	si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterNearest, MinFilter: vk.FilterNearest, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
606	if err := checked("vkCreateSampler(capture)", d.CreateSampler(r.device, &si, nil, &p.sampler)); err != nil {
607		return err
608	}
609	size := vk.DescriptorPoolSize{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1}
610	dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 1, PoolSizes: &size}
611	if err := checked("vkCreateDescriptorPool(capture)", d.CreateDescriptorPool(r.device, &dpi, nil, &p.pool)); err != nil {
612		return err
613	}
614	dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: p.pool, DescriptorSetCount: 1, SetLayouts: &p.setLayout}
615	if err := checked("vkAllocateDescriptorSets(capture)", d.AllocateDescriptorSets(r.device, &dai, &p.set)); err != nil {
616		return err
617	}
618	img := vk.DescriptorImageInfo{Sampler: p.sampler, ImageView: r.hdrOwn.view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
619	write := vk.WriteDescriptorSet{SType: vk.StructureTypeWriteDescriptorSet, DstSet: p.set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img}
620	d.UpdateDescriptorSets(r.device, 1, &write, 0, nil)
621	// The pipeline is last: it marks the pass complete.
622	return r.createGraphicsPipeline(hdrVert, captureFrag, vk.FormatB8g8r8a8Unorm, p.layout, false, &p.pipeline)
623}
624
625// destroyCapturePass frees the conversion pipeline (device idle).
626func (r *Renderer) destroyCapturePass() {
627	if r.dd == nil {
628		return
629	}
630	d, p := r.dd, &r.capturePass
631	if p.pool != 0 {
632		d.DestroyDescriptorPool(r.device, p.pool, nil)
633	}
634	if p.sampler != 0 {
635		d.DestroySampler(r.device, p.sampler, nil)
636	}
637	if p.pipeline != 0 {
638		d.DestroyPipeline(r.device, p.pipeline, nil)
639	}
640	if p.layout != 0 {
641		d.DestroyPipelineLayout(r.device, p.layout, nil)
642	}
643	if p.setLayout != 0 {
644		d.DestroyDescriptorSetLayout(r.device, p.setLayout, nil)
645	}
646	*p = hdrPass{}
647}

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