v0.4.0 b6be3a8

compose.go RawHistoryFolder

1package vulkan
2
3import (
4	_ "embed"
5	"encoding/binary"
6	"image"
7	"math"
8	"os"
9	"unsafe"
10
11	"github.com/bnema/neferwl/internal/ports"
12	vk "github.com/bnema/purego-vulkan/vulkan"
13)
14
15// Composition is one graphics pipeline drawing every piece of a frame as
16// a quad, in paint order, inside one dynamic rendering pass: solid fills,
17// client dmabufs sampled as images, and client wl_shm pixels read from a
18// GPU buffer (shm.go). Push constants carry each draw; the descriptor set
19// holds its image and buffer. Client pixels are premultiplied, hence the
20// blend ONE, ONE_MINUS_SRC_ALPHA; opaque draws force alpha to 1.
21
22//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.vert -o shaders/compose.vert.spv
23//go:generate glslc -O --target-env=vulkan1.3 shaders/compose.frag -o shaders/compose.frag.spv
24//go:generate glslc -O --target-env=vulkan1.3 shaders/compose_hdr.frag -o shaders/compose_hdr.frag.spv
25//go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.vert -o shaders/hdr.vert.spv
26//go:generate glslc -O --target-env=vulkan1.3 shaders/hdr.frag -o shaders/hdr.frag.spv
27
28var (
29	//go:embed shaders/compose.vert.spv
30	composeVert []byte
31	//go:embed shaders/compose.frag.spv
32	composeFrag []byte
33	//go:embed shaders/compose_hdr.frag.spv
34	composeHDRFrag []byte
35)
36
37// Draw modes and flags (shaders: modeSolid, modeImage, flag*).
38const (
39	modeSolid  = 0
40	modeImage  = 1
41	modeBuffer = 2
42
43	flagOpaque         = 1
44	flagExact          = 2
45	flagPQ             = 4
46	flagExtendedLinear = 8
47	flagYUV            = 16
48	flagP010           = 32
49	// flagSmooth supersamples a buffer shrunk under half size (overview
50	// previews): four bilinear samples per pixel instead of one.
51	flagSmooth = 64
52)
53
54// pushConstants is struct Draw of the shaders (std430 push constant block).
55type pushConstants struct {
56	rect  [4]int32
57	mapv  [4]float32
58	mapy  [4]float32
59	color [4]float32
60	crop  [4]float32
61	buf   [4]uint32
62	misc  [4]uint32
63}
64
65// draw is one quad of a frame.
66type draw struct {
67	pc  pushConstants
68	set vk.DescriptorSet
69	im  *imported // sampled client dmabuf, for ownership barriers
70	// acquire is the content's explicit-sync fence (nil: implicit).
71	acquire *os.File
72}
73
74// composer is the pipeline state and the objects every draw can bind.
75type composer struct {
76	setLayout   vk.DescriptorSetLayout
77	layout      vk.PipelineLayout
78	pipeline    vk.Pipeline
79	hdrPipeline vk.Pipeline
80	sampler     vk.Sampler
81	// Placeholders for the binding a draw does not use: every binding
82	// the shader declares must be valid.
83	dummyImage  vk.Image
84	dummyMemory vk.DeviceMemory
85	dummyView   vk.ImageView
86	dummyBuffer gpuBuffer
87	dummyPool   vk.DescriptorPool
88	dummySet    vk.DescriptorSet
89}
90
91func (r *Renderer) createComposer() error {
92	d, c := r.dd, &r.compose
93	bindings := []vk.DescriptorSetLayoutBinding{
94		{Binding: 0, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
95		{Binding: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
96		{Binding: 2, DescriptorType: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 1, StageFlags: vk.ShaderStageFragmentBit},
97	}
98	sli := vk.DescriptorSetLayoutCreateInfo{SType: vk.StructureTypeDescriptorSetLayoutCreateInfo, BindingCount: uint32(len(bindings)), Bindings: &bindings[0]}
99	if err := checked("vkCreateDescriptorSetLayout", d.CreateDescriptorSetLayout(r.device, &sli, nil, &c.setLayout)); err != nil {
100		return err
101	}
102	pcr := vk.PushConstantRange{StageFlags: vk.ShaderStageVertexBit | vk.ShaderStageFragmentBit, Size: uint32(unsafe.Sizeof(pushConstants{}))}
103	pli := vk.PipelineLayoutCreateInfo{SType: vk.StructureTypePipelineLayoutCreateInfo, SetLayoutCount: 1, SetLayouts: &c.setLayout, PushConstantRangeCount: 1, PushConstantRanges: &pcr}
104	if err := checked("vkCreatePipelineLayout", d.CreatePipelineLayout(r.device, &pli, nil, &c.layout)); err != nil {
105		return err
106	}
107	si := vk.SamplerCreateInfo{SType: vk.StructureTypeSamplerCreateInfo, MagFilter: vk.FilterLinear, MinFilter: vk.FilterLinear, AddressModeU: vk.SamplerAddressModeClampToEdge, AddressModeV: vk.SamplerAddressModeClampToEdge, AddressModeW: vk.SamplerAddressModeClampToEdge}
108	if err := checked("vkCreateSampler", d.CreateSampler(r.device, &si, nil, &c.sampler)); err != nil {
109		return err
110	}
111	if err := r.createPipeline(); err != nil {
112		return err
113	}
114	return r.createDummies()
115}
116
117func (r *Renderer) createPipeline() error {
118	if err := r.createGraphicsPipeline(composeVert, composeFrag, vk.FormatB8g8r8a8Unorm, r.compose.layout, true, &r.compose.pipeline); err != nil {
119		return err
120	}
121	return nil
122}
123
124func (r *Renderer) createGraphicsPipeline(vertex, fragment []byte, format vk.Format, layout vk.PipelineLayout, blend bool, pipeline *vk.Pipeline) error {
125	d := r.dd
126	vert, err := r.shaderModule(vertex)
127	if err != nil {
128		return err
129	}
130	defer d.DestroyShaderModule(r.device, vert, nil)
131	frag, err := r.shaderModule(fragment)
132	if err != nil {
133		return err
134	}
135	defer d.DestroyShaderModule(r.device, frag, nil)
136	main := []byte("main\x00")
137	stages := []vk.PipelineShaderStageCreateInfo{
138		{SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageVertexBit, Module: vert, Name: &main[0]},
139		{SType: vk.StructureTypePipelineShaderStageCreateInfo, Stage: vk.ShaderStageFragmentBit, Module: frag, Name: &main[0]},
140	}
141	vertexInput := vk.PipelineVertexInputStateCreateInfo{SType: vk.StructureTypePipelineVertexInputStateCreateInfo}
142	assembly := vk.PipelineInputAssemblyStateCreateInfo{SType: vk.StructureTypePipelineInputAssemblyStateCreateInfo, Topology: vk.PrimitiveTopologyTriangleList}
143	// The target size is fixed for the renderer's life: static viewport.
144	viewport := vk.Viewport{Width: float32(r.width), Height: float32(r.height), MaxDepth: 1}
145	scissor := vk.Rect2D{Extent: vk.Extent2D{Width: uint32(r.width), Height: uint32(r.height)}}
146	viewportState := vk.PipelineViewportStateCreateInfo{SType: vk.StructureTypePipelineViewportStateCreateInfo, ViewportCount: 1, Viewports: &viewport, ScissorCount: 1, Scissors: &scissor}
147	raster := vk.PipelineRasterizationStateCreateInfo{SType: vk.StructureTypePipelineRasterizationStateCreateInfo, PolygonMode: vk.PolygonModeFill, CullMode: vk.CullModeNone, FrontFace: vk.FrontFaceCounterClockwise, LineWidth: 1}
148	multisample := vk.PipelineMultisampleStateCreateInfo{SType: vk.StructureTypePipelineMultisampleStateCreateInfo, RasterizationSamples: vk.SampleCount1Bit}
149	attachment := vk.PipelineColorBlendAttachmentState{
150		BlendEnable:         1,
151		SrcColorBlendFactor: vk.BlendFactorOne, DstColorBlendFactor: vk.BlendFactorOneMinusSrcAlpha, ColorBlendOp: vk.BlendOpAdd,
152		SrcAlphaBlendFactor: vk.BlendFactorOne, DstAlphaBlendFactor: vk.BlendFactorOneMinusSrcAlpha, AlphaBlendOp: vk.BlendOpAdd,
153		ColorWriteMask: vk.ColorComponentRBit | vk.ColorComponentGBit | vk.ColorComponentBBit | vk.ColorComponentABit,
154	}
155	if !blend {
156		attachment.BlendEnable = 0
157	}
158	blendState := vk.PipelineColorBlendStateCreateInfo{SType: vk.StructureTypePipelineColorBlendStateCreateInfo, AttachmentCount: 1, Attachments: &attachment}
159	rendering := vk.PipelineRenderingCreateInfo{SType: vk.StructureTypePipelineRenderingCreateInfo, ColorAttachmentCount: 1, ColorAttachmentFormats: &format}
160	gpi := vk.GraphicsPipelineCreateInfo{
161		SType: vk.StructureTypeGraphicsPipelineCreateInfo, Next: unsafe.Pointer(&rendering),
162		StageCount: 2, Stages: &stages[0], VertexInputState: &vertexInput, InputAssemblyState: &assembly,
163		ViewportState: &viewportState, RasterizationState: &raster, MultisampleState: &multisample,
164		ColorBlendState: &blendState, Layout: layout, BasePipelineIndex: -1,
165	}
166	return checked("vkCreateGraphicsPipelines", d.CreateGraphicsPipelines(r.device, 0, 1, &gpi, nil, pipeline))
167}
168
169// createDummies makes the placeholder image (1×1, shader-read layout) and
170// buffer, and the set fills bind.
171func (r *Renderer) createDummies() error {
172	d, c := r.dd, &r.compose
173	ii := vk.ImageCreateInfo{SType: vk.StructureTypeImageCreateInfo, ImageType: vk.ImageType2d, Format: vk.FormatB8g8r8a8Unorm, Extent: vk.Extent3D{Width: 1, Height: 1, Depth: 1}, MipLevels: 1, ArrayLayers: 1, Samples: vk.SampleCount1Bit, Tiling: vk.ImageTilingOptimal, Usage: vk.ImageUsageSampledBit, SharingMode: vk.SharingModeExclusive, InitialLayout: vk.ImageLayoutUndefined}
174	if err := checked("vkCreateImage(dummy)", d.CreateImage(r.device, &ii, nil, &c.dummyImage)); err != nil {
175		return err
176	}
177	var req vk.MemoryRequirements
178	d.GetImageMemoryRequirements(r.device, c.dummyImage, &req)
179	kind, err := r.findMemoryType(req.MemoryTypeBits, vk.MemoryPropertyDeviceLocalBit)
180	if err != nil {
181		return err
182	}
183	alloc := vk.MemoryAllocateInfo{SType: vk.StructureTypeMemoryAllocateInfo, AllocationSize: req.Size, MemoryTypeIndex: kind}
184	if err := checked("vkAllocateMemory(dummy)", d.AllocateMemory(r.device, &alloc, nil, &c.dummyMemory)); err != nil {
185		return err
186	}
187	if err := checked("vkBindImageMemory(dummy)", d.BindImageMemory(r.device, c.dummyImage, c.dummyMemory, 0)); err != nil {
188		return err
189	}
190	if c.dummyView, err = r.imageView(c.dummyImage, vk.FormatB8g8r8a8Unorm); err != nil {
191		return err
192	}
193	if err := r.newGPUBuffer(&c.dummyBuffer, 16); err != nil {
194		return err
195	}
196	if c.dummyPool, c.dummySet, err = r.newSet(c.dummyView, c.dummyBuffer.buffer); err != nil {
197		return err
198	}
199	// Move the placeholder image to the layout every draw declares.
200	return r.oneShot(func(cmd vk.CommandBuffer) {
201		b := vk.ImageMemoryBarrier{SType: vk.StructureTypeImageMemoryBarrier, OldLayout: vk.ImageLayoutUndefined, NewLayout: vk.ImageLayoutShaderReadOnlyOptimal, DstAccessMask: vk.AccessShaderReadBit, SrcQueueFamilyIndex: queueFamilyIgnored, DstQueueFamilyIndex: queueFamilyIgnored, Image: c.dummyImage, SubresourceRange: colorRange}
202		d.CmdPipelineBarrier(cmd, vk.PipelineStageTopOfPipeBit, vk.PipelineStageFragmentShaderBit, 0, 0, nil, 0, nil, 1, &b)
203	})
204}
205
206// newSet allocates a descriptor set from its own pool (the bindings lack
207// vkFreeDescriptorSets: the pool is destroyed with the object).
208func (r *Renderer) newSet(view vk.ImageView, buffer vk.Buffer, chroma ...vk.ImageView) (vk.DescriptorPool, vk.DescriptorSet, error) {
209	d := r.dd
210	sizes := []vk.DescriptorPoolSize{{Type: vk.DescriptorTypeCombinedImageSampler, DescriptorCount: 2}, {Type: vk.DescriptorTypeStorageBuffer, DescriptorCount: 1}}
211	dpi := vk.DescriptorPoolCreateInfo{SType: vk.StructureTypeDescriptorPoolCreateInfo, MaxSets: 1, PoolSizeCount: 2, PoolSizes: &sizes[0]}
212	var pool vk.DescriptorPool
213	if err := checked("vkCreateDescriptorPool", d.CreateDescriptorPool(r.device, &dpi, nil, &pool)); err != nil {
214		return 0, 0, err
215	}
216	var set vk.DescriptorSet
217	dai := vk.DescriptorSetAllocateInfo{SType: vk.StructureTypeDescriptorSetAllocateInfo, DescriptorPool: pool, DescriptorSetCount: 1, SetLayouts: &r.compose.setLayout}
218	if err := checked("vkAllocateDescriptorSets", d.AllocateDescriptorSets(r.device, &dai, &set)); err != nil {
219		d.DestroyDescriptorPool(r.device, pool, nil)
220		return 0, 0, err
221	}
222	img := vk.DescriptorImageInfo{Sampler: r.compose.sampler, ImageView: view, ImageLayout: vk.ImageLayoutShaderReadOnlyOptimal}
223	uv := img
224	if len(chroma) > 0 {
225		uv.ImageView = chroma[0]
226	}
227	buf := vk.DescriptorBufferInfo{Buffer: buffer, Range: wholeSize}
228	writes := []vk.WriteDescriptorSet{
229		{SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 0, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &img},
230		{SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 1, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeStorageBuffer, BufferInfo: &buf},
231		{SType: vk.StructureTypeWriteDescriptorSet, DstSet: set, DstBinding: 2, DescriptorCount: 1, DescriptorType: vk.DescriptorTypeCombinedImageSampler, ImageInfo: &uv},
232	}
233	d.UpdateDescriptorSets(r.device, uint32(len(writes)), &writes[0], 0, nil)
234	return pool, set, nil
235}
236
237func (r *Renderer) destroyComposer() {
238	d, c := r.dd, &r.compose
239	if c.dummyPool != 0 {
240		d.DestroyDescriptorPool(r.device, c.dummyPool, nil)
241	}
242	r.freeGPUBuffer(&c.dummyBuffer)
243	if c.dummyView != 0 {
244		d.DestroyImageView(r.device, c.dummyView, nil)
245	}
246	if c.dummyImage != 0 {
247		d.DestroyImage(r.device, c.dummyImage, nil)
248	}
249	if c.dummyMemory != 0 {
250		d.FreeMemory(r.device, c.dummyMemory, nil)
251	}
252	if c.sampler != 0 {
253		d.DestroySampler(r.device, c.sampler, nil)
254	}
255	if c.pipeline != 0 {
256		d.DestroyPipeline(r.device, c.pipeline, nil)
257	}
258	if c.hdrPipeline != 0 {
259		d.DestroyPipeline(r.device, c.hdrPipeline, nil)
260	}
261	if c.layout != 0 {
262		d.DestroyPipelineLayout(r.device, c.layout, nil)
263	}
264	if c.setLayout != 0 {
265		d.DestroyDescriptorSetLayout(r.device, c.setLayout, nil)
266	}
267	*c = composer{}
268}
269
270func (r *Renderer) shaderModule(code []byte) (vk.ShaderModule, error) {
271	// SPIR-V is read as words: copy into a uint32-aligned slice.
272	words := make([]uint32, len(code)/4)
273	for i := range words {
274		words[i] = binary.LittleEndian.Uint32(code[i*4:])
275	}
276	info := vk.ShaderModuleCreateInfo{SType: vk.StructureTypeShaderModuleCreateInfo, CodeSize: uintptr(len(words) * 4), Code: &words[0]}
277	var m vk.ShaderModule
278	return m, checked("vkCreateShaderModule", r.dd.CreateShaderModule(r.device, &info, nil, &m))
279}
280
281var colorRange = vk.ImageSubresourceRange{AspectMask: vk.ImageAspectColorBit, LevelCount: 1, LayerCount: 1}
282
283// imageView makes a 2D color view of an image.
284func (r *Renderer) imageView(img vk.Image, format vk.Format) (vk.ImageView, error) {
285	info := vk.ImageViewCreateInfo{SType: vk.StructureTypeImageViewCreateInfo, Image: img, ViewType: vk.ImageViewType2d, Format: format, SubresourceRange: colorRange}
286	var v vk.ImageView
287	return v, checked("vkCreateImageView", r.dd.CreateImageView(r.device, &info, nil, &v))
288}
289
290// createView makes the color attachment view of a target image.
291func (r *Renderer) createView(t *target) (err error) {
292	t.view, err = r.imageView(t.image, vk.FormatB8g8r8a8Unorm)
293	return err
294}
295
296// fillDraw is a solid rect of c.
297func (r *Renderer) fillDraw(rect image.Rectangle, c [3]uint8) draw {
298	dr := draw{set: r.compose.dummySet}
299	dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
300	dr.pc.color = [4]float32{float32(c[0]) / 255, float32(c[1]) / 255, float32(c[2]) / 255, 1}
301	dr.pc.misc = [4]uint32{modeSolid, 0, uint32(r.width), uint32(r.height)}
302	return dr
303}
304
305// solidDraw is the quad of a single-pixel buffer. Opaque forces alpha to 1
306// keeping rgb, like the shader's flagOpaque does for client pixels; Fade
307// scales the four premultiplied channels. The shader decodes the rgb of a
308// solid on HDR outputs, so there it is pre-encoded such that the decode
309// yields the linear premultiplied color.
310func (r *Renderer) solidDraw(rect image.Rectangle, content *ports.SurfaceContent) draw {
311	c := *content.Solid
312	if content.Opaque {
313		c.A = 1
314	}
315	k := 1 - max(0, min(1, content.Fade))
316	c.R, c.G, c.B, c.A = c.R*k, c.G*k, c.B*k, c.A*k
317	if a := float64(c.A); r.hdrNits > 0 && a > 0 {
318		enc := func(ch float32) float32 {
319			v := max(0, min(1, float64(ch)/a))
320			return float32(linearToSRGB(srgbToLinear(v) * a))
321		}
322		c.R, c.G, c.B = enc(c.R), enc(c.G), enc(c.B)
323	}
324	dr := r.fillDraw(rect, [3]uint8{})
325	dr.pc.color = [4]float32{c.R, c.G, c.B, c.A}
326	return dr
327}
328
329// fadeSolid scales a solid draw's premultiplied color by k (a window's
330// fade). The shader decodes a solid's rgb on HDR outputs: there the rgb is
331// re-encoded so the decode yields the scaled linear premultiplied color, as
332// solidDraw does.
333func (r *Renderer) fadeSolid(dr *draw, k float32) {
334	c := &dr.pc.color
335	if r.hdrNits > 0 {
336		for i := range 3 {
337			c[i] = float32(linearToSRGB(srgbToLinear(float64(c[i])) * float64(k)))
338		}
339	} else {
340		c[0], c[1], c[2] = c[0]*k, c[1]*k, c[2]*k
341	}
342	c[3] *= k
343}
344
345// dimDraw is premultiplied black: blending ONE, ONE_MINUS_SRC_ALPHA
346// leaves the destination at (1-alpha) of its previous value.
347func (r *Renderer) dimDraw(rect image.Rectangle, alpha float64) draw {
348	dr := r.fillDraw(rect, [3]uint8{})
349	dr.pc.color[3] = float32(dimAlpha(alpha, r.hdrNits > 0))
350	return dr
351}
352
353// dimAlpha is the blend alpha of a dim. SDR blends sRGB-encoded values:
354// white becomes (1-alpha) encoded. HDR blends linear light, where the same
355// alpha looks far weaker; it gets the alpha that turns reference white into
356// the linear light of that SDR result. Brighter HDR highlights stay a
357// little brighter than an exact perceptual match.
358func dimAlpha(alpha float64, hdr bool) float64 {
359	alpha = max(0, min(1, alpha))
360	if hdr {
361		alpha = 1 - srgbToLinear(1-alpha)
362	}
363	return alpha
364}
365
366// contentDraw draws the visible rect of a w×h buffer mapped onto full
367// (target pixels) through the inverse of its buffer transform t. exact: the
368// buffer is drawn at its size.
369func (r *Renderer) contentDraw(rect, full image.Rectangle, w, h int, source [4]float32, t ports.BufferTransform, mode uint32, opaque bool) draw {
370	var dr draw
371	if source[2] <= 0 {
372		source = [4]float32{0, 0, float32(w), float32(h)}
373	}
374	// The crop in the surface's axes: its sides swap for a rotation.
375	iw, ih := t.Size(int(source[2]), int(source[3]))
376	cw, ch := float64(iw), float64(ih)
377	// src maps a target point to buffer pixels: affine, so its value at
378	// the target origin and its steps along x and y define it.
379	src := func(x, y float64) (float64, float64) {
380		bx, by := t.ToBuffer((x-float64(full.Min.X))*cw/float64(full.Dx()), (y-float64(full.Min.Y))*ch/float64(full.Dy()), cw, ch)
381		return float64(source[0]) + bx, float64(source[1]) + by
382	}
383	ox, oy := src(0, 0)
384	xx, xy := src(1, 0)
385	yx, yy := src(0, 1)
386	dr.pc.rect = [4]int32{int32(rect.Min.X), int32(rect.Min.Y), int32(rect.Max.X), int32(rect.Max.Y)}
387	dr.pc.mapv = [4]float32{float32(ox), float32(oy), float32(xx - ox), float32(xy - oy)}
388	dr.pc.mapy = [4]float32{float32(yx - ox), float32(yy - oy), 0, 0}
389	dr.pc.crop = [4]float32{source[0], source[1], source[0] + source[2], source[1] + source[3]}
390	flags := uint32(0)
391	if opaque {
392		flags |= flagOpaque
393	}
394	if float64(full.Dx()) == cw && float64(full.Dy()) == ch && cw == float64(int(cw)) && ch == float64(int(ch)) {
395		flags |= flagExact
396	}
397	if float64(full.Dx()) < cw/2 || float64(full.Dy()) < ch/2 {
398		flags |= flagSmooth
399	}
400	dr.pc.misc = [4]uint32{mode, flags, uint32(r.width), uint32(r.height)}
401	// color.a is the opacity of a content draw (wp_alpha_modifier_v1).
402	dr.pc.color[3] = 1
403	return dr
404}
405
406// recordDraws draws ds in one rendering pass that clears view to bg, or
407// keeps its contents (keep).
408func (r *Renderer) recordDraws(cmd vk.CommandBuffer, view vk.ImageView, bg [3]uint8, ds []draw, keep bool) {
409	d, c := r.dd, &r.compose
410	clear := vk.ClearValue{math.Float32bits(float32(bg[0]) / 255), math.Float32bits(float32(bg[1]) / 255), math.Float32bits(float32(bg[2]) / 255), math.Float32bits(1)}
411	if r.hdrNits > 0 {
412		for i := 0; i < 3; i++ {
413			clear[i] = math.Float32bits(float32(srgbToLinear(float64(bg[i]) / 255)))
414		}
415	}
416	attachment := vk.RenderingAttachmentInfo{SType: vk.StructureTypeRenderingAttachmentInfo, ImageView: view, ImageLayout: vk.ImageLayoutColorAttachmentOptimal, LoadOp: vk.AttachmentLoadOpClear, StoreOp: vk.AttachmentStoreOpStore, ClearValue: clear}
417	if keep {
418		attachment.LoadOp = vk.AttachmentLoadOpLoad
419	}
420	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}
421	d.CmdBeginRendering(cmd, &info)
422	pipeline := c.pipeline
423	if r.hdrNits > 0 {
424		pipeline = c.hdrPipeline
425	}
426	d.CmdBindPipeline(cmd, vk.PipelineBindPointGraphics, pipeline)
427	var bound vk.DescriptorSet
428	for i := range ds {
429		dr := &ds[i]
430		if dr.set != bound {
431			d.CmdBindDescriptorSets(cmd, vk.PipelineBindPointGraphics, c.layout, 0, 1, &dr.set, 0, nil)
432			bound = dr.set
433		}
434		d.CmdPushConstants(cmd, c.layout, vk.ShaderStageVertexBit|vk.ShaderStageFragmentBit, 0, uint32(unsafe.Sizeof(dr.pc)), unsafe.Pointer(&dr.pc))
435		d.CmdDraw(cmd, 6, 1, 0, 0)
436	}
437	d.CmdEndRendering(cmd)
438}

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