feat/hints a471fa4

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

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 23

Tags 10

↑↓ moveenter openesc close