detached fdd661a

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

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