v0.1.0 d9218d6

scanout.go RawHistoryFolder

1package drm
2
3import (
4	"errors"
5	"os"
6	"slices"
7	"time"
8	"unsafe"
9
10	"github.com/bnema/neferwl/internal/ports"
11	"golang.org/x/sys/unix"
12)
13
14// Direct scanout (ADR 006): when one fullscreen window covers the output
15// with a GPU buffer of the output's size, the output flips to that buffer
16// instead of compositing it. The buffer becomes a KMS framebuffer once
17// (PRIME import + ADDFB2) and is cached by DMABuf ID while the client
18// keeps using it. KMS waits on the buffer's implicit fences before showing
19// it. Anything else on screen, a scale, or a buffer KMS refuses falls back
20// 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// scanoutCandidate returns the window content the output can scan out
47// directly, or a reason why it cannot. w, h are the output's physical size.
48// The plane's formats are checked by scanoutFB.
49func scanoutCandidate(s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, w, h int) (ports.SurfaceContent, string) {
50	var full *ports.SceneWindow
51	for i := range s.Windows {
52		win := &s.Windows[i]
53		if win.Hidden || win.Rect.W <= 0 || win.Rect.H <= 0 {
54			continue
55		}
56		covers := win.Rect.X == 0 && win.Rect.Y == 0 && win.Rect.W == s.OutputWidth && win.Rect.H == s.OutputHeight
57		if !win.Fullscreen || !covers || full != nil {
58			return ports.SurfaceContent{}, "other_windows"
59		}
60		full = win
61	}
62	if full == nil {
63		return ports.SurfaceContent{}, "no_fullscreen"
64	}
65	for _, l := range s.Layers {
66		if l.Layer == ports.LayerOverlay && l.Rect.W > 0 && l.Rect.H > 0 {
67			return ports.SurfaceContent{}, "overlay_surface"
68		}
69	}
70	c := surfaces[full.ID]
71	switch {
72	case c.DMABuf == nil:
73		return c, "not_dmabuf"
74	case len(c.Children) > 0:
75		return c, "subsurfaces"
76	case c.Width != w || c.Height != h:
77		return c, "size_mismatch"
78	case c.LogicalW != full.Rect.W || c.LogicalH != full.Rect.H:
79		return c, "logical_mismatch"
80	case c.Geometry != (ports.Rect{}) && (c.Geometry.X != 0 || c.Geometry.Y != 0 || c.Geometry.W != c.LogicalW || c.Geometry.H != c.LogicalH):
81		return c, "geometry_crop"
82	}
83	return c, ""
84}
85
86// clientFB is a client buffer imported as a KMS framebuffer.
87type clientFB struct {
88	fbID uint32
89	last time.Time // when last wanted on screen
90	// failed is the reason KMS refused the buffer; it is not retried.
91	failed string
92	// noAsync: KMS refused an async flip to it; it flips at vblank.
93	noAsync bool
94	// overlayFailed is why the overlay plane refused it; kept apart from
95	// failed, so a refusal on one plane does not bar the other.
96	overlayFailed string
97	importErr     bool
98}
99
100// scanoutFB returns the framebuffer of a client buffer for the primary
101// plane, importing it on first use. A buffer KMS refuses is remembered
102// with its reason.
103func (o *Output) scanoutFB(b *ports.DMABuf, now time.Time) (uint32, string) {
104	return o.planeFB(o.primary, b, now, func(fb *clientFB) *string { return &fb.failed })
105}
106
107// overlayFB is scanoutFB for the overlay plane, with its own formats and
108// refusal reason.
109func (o *Output) overlayFB(b *ports.DMABuf, now time.Time) (uint32, string) {
110	return o.planeFB(o.overlay, b, now, func(fb *clientFB) *string { return &fb.overlayFailed })
111}
112
113// planeFB imports b once as a framebuffer; reason is the plane's refusal
114// field. Both planes share the framebuffer: they show the same format.
115func (o *Output) planeFB(p *plane, b *ports.DMABuf, now time.Time, reason func(*clientFB) *string) (uint32, string) {
116	fb := o.clientFBs[b.ID]
117	if fb == nil {
118		fb = &clientFB{}
119		o.clientFBs[b.ID] = fb
120	}
121	fb.last = now
122	why := reason(fb)
123	if *why != "" {
124		return 0, *why
125	}
126	format, ok := planeFormat(p, b)
127	if !ok {
128		*why = "format"
129		return 0, *why
130	}
131	if fb.fbID == 0 {
132		if fb.importErr {
133			*why = "import_failed"
134			return 0, *why
135		}
136		id, err := o.k.addFB(b, format)
137		if err != nil {
138			o.log.Info().Err(err).Uint32("format", b.Format).Uint64("modifier", b.Modifier).Msg("scanout import failed")
139			fb.importErr, *why = true, "import_failed"
140			return 0, *why
141		}
142		fb.fbID = id
143	}
144	return fb.fbID, ""
145}
146
147// scanoutFormat is the framebuffer format a client buffer is scanned out
148// as on the primary plane (see planeFormat).
149func (o *Output) scanoutFormat(b *ports.DMABuf) (uint32, bool) { return planeFormat(o.primary, b) }
150
151// planeFormat is the framebuffer format a client buffer shows as on p,
152// when p lists it with the buffer's modifier. Planes here show no alpha:
153// an alpha format goes as its X variant when the plane has that one.
154func planeFormat(p *plane, b *ports.DMABuf) (uint32, bool) {
155	has := func(f uint32) bool {
156		return slices.Contains(p.formats, ports.DMABufFormat{Format: f, Modifier: b.Modifier})
157	}
158	if x, ok := opaqueVariant[b.Format]; ok && has(x) {
159		return x, true
160	}
161	return b.Format, has(b.Format)
162}
163
164// opaqueVariant maps alpha formats to the same layout without alpha.
165var opaqueVariant = map[uint32]uint32{
166	fourccARGB: fourccXRGB,
167	fourccABGR: fourccXBGR,
168	fourccAR30: fourccXR30,
169	fourccAB30: fourccXB30,
170	fourccAR4H: fourccXR4H,
171	fourccAB4H: fourccXB4H,
172}
173
174// scanoutFormats are the formats a buffer can be scanned out with: in the
175// primary plane's IN_FORMATS (directly or as its opaque variant) and
176// composable by the renderer.
177func (o *Output) scanoutFormats(sampled []ports.DMABufFormat) []ports.DMABufFormat {
178	var out []ports.DMABufFormat
179	for _, f := range sampled {
180		b := ports.DMABuf{Format: f.Format, Modifier: f.Modifier}
181		if _, ok := o.scanoutFormat(&b); ok {
182			out = append(out, f)
183		}
184	}
185	return out
186}
187
188const (
189	fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
190	fourccARGB = 'A' | 'R'<<8 | '2'<<16 | '4'<<24
191	fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
192	fourccABGR = 'A' | 'B'<<8 | '2'<<16 | '4'<<24
193	fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
194	fourccAR30 = 'A' | 'R'<<8 | '3'<<16 | '0'<<24
195	fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
196	fourccAB30 = 'A' | 'B'<<8 | '3'<<16 | '0'<<24
197	fourccXR4H = 'X' | 'R'<<8 | '4'<<16 | 'H'<<24
198	fourccAR4H = 'A' | 'R'<<8 | '4'<<16 | 'H'<<24
199	fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
200	fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
201)
202
203// addFB imports a dmabuf as a framebuffer of the given fourcc: client buffers, the renderer's
204// output images and cursor images. The GEM handle belongs to the card fd
205// and is shared by every import of the same buffer, so import, ADDFB2 and
206// close run under the lock and the handle is closed right away: the
207// framebuffer keeps its own reference to the buffer.
208func (k kmsDevice) addFB(b *ports.DMABuf, format uint32) (uint32, error) {
209	raw, err := b.Planes[0].File.SyscallConn()
210	if err != nil {
211		return 0, err
212	}
213	k.gemMu.Lock()
214	defer k.gemMu.Unlock()
215	var p primeHandle
216	var ioErr error
217	if err := raw.Control(func(fd uintptr) {
218		p.fd = int32(fd)
219		ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
220	}); err != nil {
221		return 0, err
222	}
223	if ioErr != nil {
224		return 0, errors.Join(errors.New("prime import"), ioErr)
225	}
226	defer func() {
227		g := gemClose{handle: p.handle}
228		_ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
229	}()
230	cmd := fbCmd2{width: uint32(b.Width), height: uint32(b.Height), format: format}
231	useMod := b.Modifier != modInvalid && (b.Modifier != 0 || k.modifiers)
232	if useMod {
233		cmd.flags = fbModifiers
234	}
235	// Planes in the same dmabuf share its GEM handle: each distinct
236	// handle is closed once.
237	closing := map[uint32]bool{p.handle: true}
238	for i, pl := range b.Planes {
239		if i >= len(cmd.handles) {
240			break
241		}
242		h := p.handle
243		if i > 0 {
244			var err error
245			if h, err = k.primeImport(pl.File); err != nil {
246				return 0, err
247			}
248			if !closing[h] {
249				closing[h] = true
250				defer k.gemClose(h)
251			}
252		}
253		cmd.handles[i], cmd.pitches[i], cmd.offsets[i] = h, pl.Stride, pl.Offset
254		if useMod {
255			cmd.modifiers[i] = b.Modifier
256		}
257	}
258	if err := ioctl(k.fd, ioctlAddFB2, unsafe.Pointer(&cmd)); err != nil {
259		return 0, errors.Join(errors.New("add fb2"), err)
260	}
261	return cmd.fbID, nil
262}
263
264// primeImport returns the GEM handle of a dmabuf file. The caller holds
265// gemMu and closes the handle.
266func (k kmsDevice) primeImport(f *os.File) (uint32, error) {
267	raw, err := f.SyscallConn()
268	if err != nil {
269		return 0, err
270	}
271	var p primeHandle
272	var ioErr error
273	if err := raw.Control(func(fd uintptr) {
274		p.fd = int32(fd)
275		ioErr = ioctl(k.fd, ioctlPrimeFDToHandle, unsafe.Pointer(&p))
276	}); err != nil {
277		return 0, err
278	}
279	if ioErr != nil {
280		return 0, errors.Join(errors.New("prime import"), ioErr)
281	}
282	return p.handle, nil
283}
284
285func (k kmsDevice) gemClose(h uint32) {
286	g := gemClose{handle: h}
287	_ = ioctl(k.fd, ioctlGemClose, unsafe.Pointer(&g))
288}
289
290// dropClientFBs frees framebuffers not wanted for scanoutIdleTTL, or all
291// of them, except those on screen or queued.
292func (o *Output) dropClientFBs(now time.Time, all bool) {
293	for id, fb := range o.clientFBs {
294		if id == o.shown || id == o.queued {
295			continue
296		}
297		if !all && now.Sub(fb.last) < scanoutIdleTTL {
298			continue
299		}
300		if fb.fbID != 0 {
301			_ = o.k.rmFB(fb.fbID)
302		}
303		delete(o.clientFBs, id)
304	}
305}
306
307// dupFence duplicates a sync file under its SyscallConn, so a concurrent
308// Close cannot hand us a reused fd; nil when f is nil or closed.
309func dupFence(f *os.File) *os.File {
310	if f == nil {
311		return nil
312	}
313	raw, err := f.SyscallConn()
314	if err != nil {
315		return nil
316	}
317	fd := -1
318	if raw.Control(func(v uintptr) { fd, _ = unix.FcntlInt(v, unix.F_DUPFD_CLOEXEC, 0) }) != nil || fd < 0 {
319		return nil
320	}
321	return os.NewFile(uintptr(fd), "acquire-fence")
322}

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