v0.10.0 d5d679d

scanout.go RawHistoryFolder

1package drm
2
3import (
4	"errors"
5	"math"
6	"os"
7	"slices"
8	"time"
9	"unsafe"
10
11	"github.com/bnema/neferwl/internal/ports"
12	"golang.org/x/sys/unix"
13)
14
15// Direct scanout (ADR 006): when one fullscreen window covers the output
16// with a GPU buffer whose source can fill the plane, the output flips to that buffer
17// instead of compositing it. The buffer becomes a KMS framebuffer once
18// (PRIME import + ADDFB2) and is cached by DMABuf ID while the client
19// keeps using it. KMS waits on the buffer's implicit fences before showing
20// it. Anything else on screen or a buffer KMS refuses falls back to composition.
21
22const (
23	ioctlGemClose        = 0x40086409
24	ioctlPrimeFDToHandle = 0xC00C642E
25	ioctlAddFB2          = 0xC06864B8
26
27	fbModifiers        = 1 << 1
28	capAddFB2Modifiers = 0x10
29	modInvalid         = 0x00ffffffffffffff
30	scanoutIdleTTL     = 5 * time.Second // a cached framebuffer survives unused
31)
32
33type primeHandle struct {
34	handle, flags uint32
35	fd            int32
36}
37
38type gemClose struct{ handle, pad uint32 }
39
40type fbCmd2 struct {
41	fbID, width, height, format, flags uint32
42	handles, pitches, offsets          [4]uint32
43	modifiers                          [4]uint64
44}
45
46// planeRect contains source coordinates in 16.16 and destination pixels.
47type planeRect struct {
48	src  [4]uint64
49	crtc [4]uint64
50}
51
52func fullPlaneRect(w, h int) planeRect {
53	return planeRect{src: [4]uint64{0, 0, uint64(w) << 16, uint64(h) << 16}, crtc: [4]uint64{0, 0, uint64(w), uint64(h)}}
54}
55
56func scanoutRect(c ports.SurfaceContent, w, h int) planeRect {
57	r := fullPlaneRect(w, h)
58	r.src = [4]uint64{0, 0, uint64(c.Width) << 16, uint64(c.Height) << 16}
59	if c.Source[2] != 0 {
60		for i, value := range c.Source {
61			r.src[i] = uint64(float64(value) * 65536)
62		}
63	}
64	return r
65}
66
67// covers reports whether win fills the output of scene s.
68func covers(s *ports.Scene, win *ports.SceneWindow) bool {
69	return (s.WorkspaceClip == (ports.Rect{}) || s.WorkspaceClip == (ports.Rect{W: s.OutputWidth, H: s.OutputHeight})) &&
70		win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight
71}
72
73// fullscreenShown reports whether a visible fullscreen window covers the
74// output, whether its frame is scanned out or composed.
75func fullscreenShown(s *ports.Scene) bool {
76	for i := range s.Windows {
77		win := &s.Windows[i]
78		if win.Fullscreen && !win.Hidden && covers(s, win) {
79			return true
80		}
81	}
82	return false
83}
84
85// scanoutCandidate returns the window content the output can scan out
86// directly, or a reason why it cannot. w, h are the output's physical size.
87// The plane's formats are checked by scanoutFB.
88//
89// kept reports a window drawn from a content its client withdrew (it
90// closed and fades out): the client may already have its buffer back, so
91// it is never put on a plane (nil: none is).
92func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int, kept func(ports.WindowID) bool) (ports.SurfaceContent, string) {
93	// A hardware plane would bypass the compositor-owned flash border.
94	if s.Flash != nil {
95		return ports.SurfaceContent{}, "focus_flash"
96	}
97	// Hint labels are composed over the window.
98	if len(s.HintLabels) > 0 {
99		return ports.SurfaceContent{}, "hint_labels"
100	}
101	if s.Transform != 0 {
102		// The scanout buffer would have to be pre-rotated: rotated outputs compose.
103		return ports.SurfaceContent{}, "output_transform"
104	}
105	if s.WorkspaceClip != (ports.Rect{}) && s.WorkspaceClip != (ports.Rect{W: s.OutputWidth, H: s.OutputHeight}) {
106		return ports.SurfaceContent{}, "workspace_clip"
107	}
108	var full *ports.SceneWindow
109	for i := range s.Windows {
110		win := &s.Windows[i]
111		if !s.Draws(*win) {
112			continue
113		}
114		if !win.Fullscreen || !covers(&s, win) || full != nil {
115			return ports.SurfaceContent{}, "other_windows"
116		}
117		full = win
118	}
119	if full == nil {
120		return ports.SurfaceContent{}, "no_fullscreen"
121	}
122	if kept != nil && kept(full.ID) {
123		return ports.SurfaceContent{}, "leaving"
124	}
125	if len(s.DropHints) > 0 {
126		// Drag hints are drawn over the window.
127		return ports.SurfaceContent{}, "drop_hint"
128	}
129	if full.Dim > 0 {
130		// Its veil is composed over it: the plane would show it bright.
131		return ports.SurfaceContent{}, "dim"
132	}
133	if full.Fade > 0 || full.Preview > 0 || full.Zoom > 0 {
134		// A fading or zoomed window is composed: the plane would show it
135		// whole and opaque.
136		return ports.SurfaceContent{}, "window_fade"
137	}
138	// Core leaves the layers a fullscreen window hides out of the scene:
139	// one above the windows here is drawn.
140	for _, l := range s.Layers {
141		if l.Layer >= ports.LayerTop && l.Rect.W > 0 && l.Rect.H > 0 {
142			return ports.SurfaceContent{}, "overlay_surface"
143		}
144	}
145	c := surfaces[full.ID]
146	if c.DMABuf == nil && len(c.Children) > 0 {
147		var reason string
148		if c, reason = clientSubsurface(c); reason != "" {
149			return c, reason
150		}
151	}
152	switch {
153	case c.DMABuf == nil:
154		return c, "not_dmabuf"
155	case c.Fade > 0:
156		// The plane would show it unfaded (wp_alpha_modifier_v1).
157		return c, "fade"
158	case len(c.Children) > 0:
159		return c, "subsurfaces"
160	case c.Transform != 0:
161		// Planes show buffers as they are: rotation is composed.
162		return c, "buffer_transform"
163	case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:
164		return c, "letterbox"
165	case c.Width <= 0 || c.Height <= 0 || c.Source[2] < 0 || c.Source[3] < 0 || !finiteSource(c.Source):
166		return c, "source_crop"
167	case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):
168		return c, "geometry_crop"
169	}
170	if c.Source[2] != 0 && (c.Source[0] < 0 || c.Source[1] < 0 || c.Source[2] <= 0 || c.Source[3] <= 0 || c.Source[0]+c.Source[2] > float32(c.Width) || c.Source[1]+c.Source[3] > float32(c.Height)) {
171		return c, "source_crop"
172	}
173	return c, ""
174}
175
176func finiteSource(src [4]float32) bool {
177	for _, n := range src {
178		if math.IsNaN(float64(n)) || math.IsInf(float64(n), 0) {
179			return false
180		}
181	}
182	return true
183}
184
185// cropped reports whether a viewport source shows less than the buffer.
186func cropped(c ports.SurfaceContent) bool {
187	return c.Source[2] != 0 && c.Source != [4]float32{0, 0, float32(c.Width), float32(c.Height)}
188}
189
190// clientSubsurface returns the content to scan out for a window whose
191// root has no GPU buffer but one opaque subsurface over its geometry: Wine
192// draws the frame in the root (wl_shm) and presents Vulkan in a subsurface
193// on the client area. That subsurface hides the root entirely, so it is
194// what the screen shows. The returned content keeps the window's ID and
195// Seq for release and presentation.
196func clientSubsurface(c ports.SurfaceContent) (ports.SurfaceContent, string) {
197	if len(c.Children) != 1 {
198		return c, "subsurfaces"
199	}
200	ch := c.Children[0]
201	switch {
202	case ch.DMABuf == nil:
203		return c, "not_dmabuf"
204	case !ch.Opaque:
205		return c, "subsurface_translucent"
206	case ch.Below:
207		// The root is drawn over it.
208		return c, "subsurface_below"
209	case ch.X != c.Geometry.X || ch.Y != c.Geometry.Y:
210		return c, "subsurface_offset"
211	}
212	out := ch.SurfaceContent
213	out.ID, out.Seq, out.Geometry = c.ID, c.Seq, ports.Rect{}
214	return out, ""
215}
216
217// scanoutFrame picks a fullscreen client buffer to flip directly. It
218// returns fb 0, with the reason logged on change, when the frame must be
219// composed.
220func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (fb uint32, c ports.SurfaceContent, mode colorMode) {
221	reason := "disabled"
222	if o.scanout {
223		c, reason = scanoutCandidate(scene, surfaces, o.Width(), o.Height(), o.kept)
224		if reason == "" {
225			mode, reason = planeColor(c.Color, c.DMABuf.Format, c.Opaque, o.hdr.on, o.primary.pipeline)
226		}
227	}
228	if reason == "" {
229		fb, reason = o.scanoutFB(c.DMABuf, time.Now())
230		if reason == "" {
231			rect := scanoutRect(c, o.Width(), o.Height())
232			cfb := o.clientFBs[c.DMABuf.ID]
233			if cfb.scaleRefused && cfb.scaleFailed == rect {
234				reason = "scale_refused"
235			} else if rect != fullPlaneRect(o.Width(), o.Height()) && (!cfb.scaleTestedOK || cfb.scaleTested != rect) {
236				req := &o.probeReq
237				req.reset()
238				o.primaryRectProps(req, fb, rect)
239				if err := o.k.commit(req, atomicTestOnly, 0); err != nil {
240					cfb.scaleFailed = rect
241					cfb.scaleRefused = true
242					reason = "scale_refused"
243					o.renderLog.Info().Err(err).Str("reason", reason).Str("connector", o.conn.name).Msg("scanout scaling refused")
244				} else {
245					cfb.scaleTested = rect
246					cfb.scaleTestedOK = true
247				}
248			}
249			if reason == "" && mode != colorBypass && !o.colorAllowed(o.primary, c.DMABuf.Format, func(req *atomicReq) {
250				o.primaryRectProps(req, fb, rect)
251				if o.overlayOn != 0 {
252					o.overlayProps(req, overlayWin{})
253				}
254			}) {
255				reason = "color_refused"
256			}
257		}
258	}
259	o.setScanoutReason(reason)
260	if reason != "" {
261		return 0, c, colorBypass
262	}
263	return fb, c, mode
264}
265
266// overlayFrame decides the overlay of a frame: the window, and the scene
267// the renderer composes (the window left out). A zero overlayWin means
268// none; the reason is logged on change. The returned scene's Windows is
269// valid until the next call: it reuses o.restWindows (its consumers, Render,
270// ExcludedScene and the capture tracking, run synchronously before then).
271func (o *Output) overlayFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) (overlayWin, ports.Scene) {
272	reason := "no_plane"
273	var ov overlayWin
274	if o.overlay != nil && o.scanout {
275		var w ports.SceneWindow
276		var c ports.SurfaceContent
277		var mode colorMode
278		w, c, mode, reason = overlayCandidate(s, surfaces, o.hdr.on, o.overlay.pipeline, o.kept)
279		if reason == "" && mode != colorBypass {
280			if ok, known := o.overlay.verdict(c.DMABuf.Format, o.cursorShown()); known && !ok {
281				reason = "color_refused"
282			}
283		}
284		if reason == "" {
285			var fb uint32
286			if fb, reason = o.overlayFB(c.DMABuf, time.Now()); reason == "" {
287				scale := s.Scale
288				if scale <= 0 {
289					scale = 1
290				}
291				ov = overlayWin{id: w.ID, fb: fb, buf: c.DMABuf.ID, w: c.Width, h: c.Height,
292					rect:    ports.Rect{X: int(float64(w.Rect.X) * scale), Y: int(float64(w.Rect.Y) * scale), W: c.Width, H: c.Height},
293					acquire: dupFence(c.Acquire), color: colorUse{mode: mode, format: c.DMABuf.Format}}
294			}
295		}
296	}
297	o.setOverlayReason(reason)
298	if ov.id == 0 {
299		return overlayWin{}, s
300	}
301	// The composed frame leaves the window out: the overlay shows it.
302	rest := s
303	o.restWindows = o.restWindows[:0]
304	for _, w := range s.Windows {
305		if w.ID != ov.id {
306			o.restWindows = append(o.restWindows, w)
307		}
308	}
309	rest.Windows = o.restWindows
310	return ov, rest
311}
312
313// frameDecision selects the zero-copy path or the scene to compose.
314type frameDecision struct {
315	fb       uint32
316	content  ports.SurfaceContent
317	rect     planeRect
318	color    colorMode // how the primary plane shows fb
319	overlay  overlayWin
320	composed ports.Scene
321}
322
323// composeFrame chooses overlay or composition after direct scanout cannot commit.
324// The caller owns the returned overlay fence.
325func (o *Output) composeFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent) frameDecision {
326	ov, rest := o.overlayFrame(s, surfaces)
327	if ov.fb != 0 {
328		return frameDecision{overlay: ov, composed: rest}
329	}
330	return frameDecision{composed: s}
331}
332
333// decideFrame tries direct scanout, then overlay, then composition.
334// Captures require composition; the caller owns the returned overlay fence.
335func (o *Output) decideFrame(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, capture bool) frameDecision {
336	if capture {
337		o.setScanoutReason("capture")
338		o.setOverlayReason("capture")
339		return frameDecision{composed: s}
340	}
341	fb, c, mode := o.scanoutFrame(s, surfaces)
342	if fb != 0 {
343		return frameDecision{fb: fb, content: c, rect: scanoutRect(c, o.Width(), o.Height()), color: mode}
344	}
345	return o.composeFrame(s, surfaces)
346}
347
348// setScanoutReason logs a direct-scanout transition once.
349func (o *Output) setScanoutReason(reason string) {
350	if reason != o.reason {
351		o.renderLog.Info().Bool("direct_scanout", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("scanout")
352		o.reason = reason
353	}
354}
355
356// setOverlayReason logs an overlay transition once.
357func (o *Output) setOverlayReason(reason string) {
358	if reason != o.overlayReason {
359		o.renderLog.Info().Bool("overlay", reason == "").Str("reason", reason).Str("connector", o.conn.name).Msg("overlay")
360		o.overlayReason = reason
361	}
362}
363
364// clientFB is a client buffer imported as a KMS framebuffer.
365type clientFB struct {
366	fbID uint32
367	last time.Time // when last wanted on screen
368	// failed is the reason KMS refused the buffer; it is not retried.
369	failed string
370	// noAsync: KMS refused an async flip to it; it flips at vblank.
371	noAsync                     bool
372	scaleTested, scaleFailed    planeRect
373	scaleRefused, scaleTestedOK bool
374	// overlayFailed is why the overlay plane refused it; kept apart from
375	// failed, so a refusal on one plane does not bar the other.
376	overlayFailed string
377	importErr     bool
378}
379
380// scanoutFB returns the framebuffer of a client buffer for the primary
381// plane, importing it on first use. A buffer KMS refuses is remembered
382// with its reason.
383func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
384	return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed })
385}
386
387// overlayFB is scanoutFB for the overlay plane, with its own formats and
388// refusal reason.
389func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) {
390	return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed })
391}
392
393// planeFB imports b once as a framebuffer; reason is the plane's refusal
394// field. Both planes share the framebuffer: they show the same format.
395func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) {
396	fb := o.clientFBs[b.ID]
397	if fb == nil {
398		fb = &clientFB{}
399		o.clientFBs[b.ID] = fb
400	}
401	fb.last = now
402	why := reason(fb)
403	if *why != "" {
404		return 0, *why
405	}
406	format, ok := planeFormat(p, b)
407	if !ok {
408		*why = "format"
409		return 0, *why
410	}
411	if fb.fbID == 0 {
412		if fb.importErr {
413			*why = "import_failed"
414			return 0, *why
415		}
416		id, err := o.k.addFB(b, format)
417		if err != nil {
418			o.renderLog.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
419			fb.importErr, *why = true, "import_failed"
420			return 0, *why
421		}
422		fb.fbID = id
423	}
424	return fb.fbID, ""
425}
426
427// scanoutFormat is the framebuffer format a client buffer is scanned out
428// as on the primary plane (see planeFormat).
429func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) }
430
431// planeFormat is the framebuffer format a client buffer shows as on p,
432// when p lists it with the buffer's modifier. Planes here show no alpha:
433// an alpha format goes as its X variant when the plane has that one.
434func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) {
435	has := func(f uint32) bool {
436		return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier})
437	}
438	if x, ok := opaqueVariant[b.Format]; ok && has(x) {
439		return x, true
440	}
441	return b.Format, has(b.Format)
442}
443
444// opaqueVariant maps alpha formats to the same layout without alpha.
445var opaqueVariant = map[uint32]uint32{
446	fourccARGB: fourccXRGB,
447	fourccABGR: fourccXBGR,
448	fourccAR30: fourccXR30,
449	fourccAB30: fourccXB30,
450	fourccAR4H: fourccXR4H,
451	fourccAB4H: fourccXB4H,
452}
453
454// scanoutFormats are the formats a buffer can be scanned out with: in the
455// primary plane's IN_FORMATS (directly or as its opaque variant) and
456// composable by the renderer.
457func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat {
458	var out []ports.DMABufFormat
459	for _, f := range sampled {
460		b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier}
461		if _, ok := o.scanoutFormat(&b); ok {
462			out = append(out, f)
463		}
464	}
465	return out
466}
467
468// isYUVFormat identifies video formats which are always composed, never scanned out.
469func isYUVFormat(format uint32) bool {
470	return format == fourccNV12 || format == fourccP010
471}
472
473// isTenBit identifies the packed 2101010 formats supported by the renderer.
474func isTenBit(format uint32) bool {
475	switch format {
476	case fourccXR30, fourccAR30, fourccXB30, fourccAB30:
477		return true
478	}
479	return false
480}
481
482const (
483	fourccNV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
484	fourccP010 = 'P' | '0'<<8 | '1'<<16 | '0'<<24
485	fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
486	fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
487	fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
488	fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24
489	fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
490	fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24
491	fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
492	fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24
493	fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24
494	fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24
495	fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
496	fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
497)
498
499// addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's
500// output images and cursor images. The GEM handle belongs to the card fd
501// and is shared by every import of the same buffer, so import, ADDFB2 and
502// close run under the lock and the handle is closed right away: the
503// framebuffer keeps its own reference to the buffer.
504func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
505	raw, err := b.Planes[0].File.SyscallConn()
506	if err != nil {
507		return 0, err
508	}
509	k.gemMu.Lock()
510	defer k.gemMu.Unlock()
511	var p primeHandle
512	var ioErr error
513	if err := raw.Control(func(fd uintptr) {
514		p.fd = int32(fd)
515		ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
516	}); err != nil {
517		return 0, err
518	}
519	if ioErr != nil {
520		return 0, errors.Join(errors.New("prime import"), ioErr)
521	}
522	defer func() {
523		g := gemClose{handle: p.handle}
524		_ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
525	}()
526	cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}
527	useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers)
528	if useMod {
529		cmd.flags = fbModifiers
530	}
531	// Planes in the same dmabuf share its GEM handle: each distinct
532	// handle is closed once.
533	closing := map[uint32]bool{p.handle: true}
534	for i, pl := range b.Planes {
535		if i >= len(cmd.handles) {
536			break
537		}
538		h := p.handle
539		if i > 0 {
540			var err error
541			if h, err = k.primeImport(pl.File); err != nil {
542				return 0, err
543			}
544			if !closing[h] {
545				closing[h] = true
546				defer k.gemClose(h)
547			}
548		}
549		cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
550		if useMod {
551			cmd.modifiers[i] = b.Modifier
552		}
553	}
554	if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
555		return 0, errors.Join(errors.New("add fb2"), err)
556	}
557	return cmd.fbID, nil
558}
559
560// primeImport returns the GEM handle of a dmabuf file. The caller holds
561// gemMu and closes the handle.
562func (k kmsDevice) primeImport(f *os.File) (uint32, error) {
563	raw, err := f.SyscallConn()
564	if err != nil {
565		return 0, err
566	}
567	var p primeHandle
568	var ioErr error
569	if err := raw.Control(func(fd uintptr) {
570		p.fd = int32(fd)
571		ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
572	}); err != nil {
573		return 0, err
574	}
575	if ioErr != nil {
576		return 0, errors.Join(errors.New("prime import"), ioErr)
577	}
578	return p.handle, nil
579}
580
581func (k kmsDevice) gemClose(h uint32) {
582	g := gemClose{handle: h}
583	_ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
584}
585
586// dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all
587// of them, except those on screen or queued.
588func (o *Output) dropClientFBs(now time.Time, all bool) {
589	for id, fb := range o.clientFBs {
590		if id == o.shown || id == o.queued {
591			continue
592		}
593		if !all && now.Sub(fb.last) < scanoutIdleTTL {
594			continue
595		}
596		if fb.fbID != 0 {
597			_ = o.k.rmFB(fb.fbID)
598			// KMS may give the id to another buffer.
599			o.forgetOverlayProbes()
600		}
601		delete(o.clientFBs, id)
602	}
603}
604
605// dupFence duplicates a sync file under its SyscallConn, so a concurrent
606// Close cannot hand us a reused fd; nil when f is nil or closed.
607func dupFence(f *os.File) *os.File {
608	if f == nil {
609		return nil
610	}
611	raw, err := f.SyscallConn()
612	if err != nil {
613		return nil
614	}
615	fd := -1
616	if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 {
617		return nil
618	}
619	return os.NewFile(uintptr(fd), "acquire-fence")
620}

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