dmabuf.go RawHistoryFolder

1package wayland
2
3import (
4	"encoding/binary"
5	"io"
6	"os"
7	"slices"
8
9	"github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/bnema/purego-libwayland/server"
13	"golang.org/x/sys/unix"
14)
15
16// linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.
17// The formats are those the renderer imports (ports.DMABufSupport); without
18// any, the global is not advertised and clients use wl_shm. Surface
19// feedback also offers, first, a scanout tranche while the surface's
20// window is fullscreen on an output that reported scanout formats
21// (ports.OutputFormats): a buffer allocated from it can be flipped to the
22// display with no composition.
23
24const dmabufVersion = 4
25
26// maxPlanes is the protocol limit (DRM planes).
27const maxPlanes = 4
28
29func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error {
30	if len(sup.Formats) == 0 {
31		return nil
32	}
33	table, err := formatTable(sup.Formats)
34	if err != nil {
35		return err
36	}
37	s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table, scanout: map[string]ports.OutputFormats{}, feedbacks: map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface{}}
38	return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
39		r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
40		if err != nil || v >= 4 {
41			return
42		}
43		// Before v4 formats come as events on bind.
44		for _, f := range sup.Formats {
45			if v >= 3 {
46				r.SendModifier(f.Format, uint32(f.Modifier>>32), uint32(f.Modifier))
47			} else if f.Modifier == 0 {
48				r.SendFormat(f.Format)
49			}
50		}
51	})
52}
53
54// formatTable is the shared memfd of 16-byte entries (u32 format, 4 bytes
55// padding, u64 modifier) feedback clients map read-only.
56func formatTable(formats []ports.DMABufFormat) (*os.File, error) {
57	data := make([]byte, 16*len(formats))
58	for i, f := range formats {
59		binary.NativeEndian.PutUint32(data[i*16:], f.Format)
60		binary.NativeEndian.PutUint64(data[i*16+8:], f.Modifier)
61	}
62	fd, err := unix.MemfdCreate("neferwl-dmabuf-formats", unix.MFD_CLOEXEC|unix.MFD_ALLOW_SEALING)
63	if err != nil {
64		return nil, err
65	}
66	f := os.NewFile(uintptr(fd), "dmabuf-formats")
67	if _, err := f.Write(data); err != nil {
68		f.Close()
69		return nil, err
70	}
71	// Clients map it: it must never shrink or change.
72	if _, err := unix.FcntlInt(uintptr(fd), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK|unix.F_SEAL_GROW|unix.F_SEAL_WRITE|unix.F_SEAL_SEAL); err != nil {
73		f.Close()
74		return nil, err
75	}
76	return f, nil
77}
78
79type dmabufGlobal struct {
80	server  *Server
81	support ports.DMABufSupport
82	table   *os.File
83	nextID  uint64
84	// scanout are the outputs' direct scanout formats by output name.
85	scanout map[string]ports.OutputFormats
86	// feedbacks are the live feedback objects and their surface (nil:
87	// default feedback).
88	feedbacks map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface
89}
90
91func (g *dmabufGlobal) close() {
92	if g != nil && g.table != nil {
93		g.table.Close()
94	}
95}
96
97func (*dmabufGlobal) Destroy(*linuxdmabuf.ZwpLinuxDmabufV1) {}
98
99func (g *dmabufGlobal) CreateParams(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
100	p := &params{global: g}
101	res, err := linuxdmabuf.NewZwpLinuxBufferParamsV1(r.Client(), r.Version(), id, p)
102	if err != nil {
103		return
104	}
105	p.resource = res
106	res.OnDestroy = p.closeFiles
107}
108
109func (g *dmabufGlobal) GetDefaultFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32) {
110	g.feedback(r, id, nil)
111}
112
113// GetSurfaceFeedback follows the surface: a scanout tranche comes first
114// while it can be scanned out (see scanoutFor).
115func (g *dmabufGlobal) GetSurfaceFeedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *wayland.Surface) {
116	g.feedback(r, id, g.server.surfaceOf(surf))
117}
118
119func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf *surface) {
120	fb, err := linuxdmabuf.NewZwpLinuxDmabufFeedbackV1(r.Client(), r.Version(), id, feedback{})
121	if err != nil {
122		return
123	}
124	g.feedbacks[fb] = surf
125	fb.OnDestroy = func() { delete(g.feedbacks, fb) }
126	g.send(fb, surf)
127}
128
129// devBytes is a dev_t as the protocol's array.
130func devBytes(dev uint64) []byte {
131	b := make([]byte, 8)
132	binary.NativeEndian.PutUint64(b, dev)
133	return b
134}
135
136// send sends the whole feedback: table, main device, the scanout tranche
137// when the surface has one, then the renderer tranche.
138func (g *dmabufGlobal) send(fb *linuxdmabuf.ZwpLinuxDmabufFeedbackV1, surf *surface) {
139	fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
140	main := devBytes(g.support.Device)
141	fb.SendMainDevice(main)
142	if out, ok := g.scanoutFor(surf); ok {
143		var indices []byte
144		for _, f := range out.Formats {
145			if i := slices.Index(g.support.Formats, f); i >= 0 {
146				indices = binary.NativeEndian.AppendUint16(indices, uint16(i))
147			}
148		}
149		if len(indices) > 0 {
150			fb.SendTrancheTargetDevice(devBytes(out.Device))
151			fb.SendTrancheFlags(uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout))
152			fb.SendTrancheFormats(indices)
153			fb.SendTrancheDone()
154		}
155	}
156	indices := make([]byte, 2*len(g.support.Formats))
157	for i := range g.support.Formats {
158		binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))
159	}
160	fb.SendTrancheTargetDevice(main)
161	fb.SendTrancheFlags(0)
162	fb.SendTrancheFormats(indices)
163	fb.SendTrancheDone()
164	fb.SendDone()
165}
166
167// scanoutFor is the scanout offer for a surface: its window is fullscreen
168// on an output that reported scanout formats.
169func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {
170	if surf == nil || surf.destroyed {
171		return ports.OutputFormats{}, false
172	}
173	root := toplevelRoot(surf)
174	if g.server.invisible(surf) || root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen {
175		return ports.OutputFormats{}, false
176	}
177	out, ok := g.scanout[root.xdg.window.last.Output]
178	return out, ok && len(out.Formats) > 0
179}
180
181// setOutputFormats records an output's scanout formats and sends the
182// feedback of the surfaces fullscreen on it again. Outputs offer only
183// formats the renderer samples (a refused buffer is composed), so they
184// are all in the table.
185func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) {
186	if old, ok := g.scanout[f.Output]; ok && old.Device == f.Device && slices.Equal(old.Formats, f.Formats) {
187		return
188	}
189	if len(f.Formats) == 0 {
190		delete(g.scanout, f.Output)
191	} else {
192		g.scanout[f.Output] = f
193	}
194	for fb, surf := range g.feedbacks {
195		if surf != nil && !surf.destroyed {
196			if root := toplevelRoot(surf); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Fullscreen && root.xdg.window.last.Output == f.Output {
197				g.send(fb, surf)
198			}
199		}
200	}
201}
202
203// resendSurface sends the feedback of a window's surfaces again, when its
204// fullscreen state or output changed.
205func (g *dmabufGlobal) resendSurface(root *surface) {
206	if g == nil {
207		return
208	}
209	for fb, surf := range g.feedbacks {
210		// Popups follow their toplevel's offer (scanoutFor).
211		if surf != nil && toplevelRoot(surf) == root {
212			g.send(fb, surf)
213		}
214	}
215}
216
217type feedback struct{}
218
219func (feedback) Destroy(*linuxdmabuf.ZwpLinuxDmabufFeedbackV1) {}
220
221// params collects the planes of one buffer.
222type params struct {
223	global   *dmabufGlobal
224	resource *linuxdmabuf.ZwpLinuxBufferParamsV1
225	planes   [maxPlanes]*plane
226	modifier uint64
227	used     bool
228}
229
230type plane struct {
231	file           *os.File
232	offset, stride uint32
233}
234
235func (p *params) closeFiles() {
236	for i, pl := range p.planes {
237		if pl != nil {
238			pl.file.Close()
239			p.planes[i] = nil
240		}
241	}
242}
243
244func (p *params) Destroy(*linuxdmabuf.ZwpLinuxBufferParamsV1) {}
245
246func (p *params) Add(r *linuxdmabuf.ZwpLinuxBufferParamsV1, fd int, idx, offset, stride, modHi, modLo uint32) {
247	f := os.NewFile(uintptr(fd), "dmabuf")
248	switch {
249	case p.used:
250		f.Close()
251		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed), "params already used")
252	case idx >= maxPlanes:
253		f.Close()
254		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorPlaneIdx), "plane index out of bounds")
255	case p.planes[idx] != nil:
256		f.Close()
257		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorPlaneSet), "plane already set")
258	default:
259		mod := uint64(modHi)<<32 | uint64(modLo)
260		if p.anyPlane() && mod != p.modifier {
261			f.Close()
262			r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat), "planes with different modifiers")
263			return
264		}
265		p.modifier = mod
266		p.planes[idx] = &plane{file: f, offset: offset, stride: stride}
267	}
268}
269
270func (p *params) anyPlane() bool {
271	return slices.ContainsFunc(p.planes[:], func(pl *plane) bool { return pl != nil })
272}
273
274const (
275	fourccNV12 = 'N' | 'V'<<8 | '1'<<16 | '2'<<24
276	fourccP010 = 'P' | '0'<<8 | '1'<<16 | '0'<<24
277	fourccAB4H = 'A' | 'B'<<8 | '4'<<16 | 'H'<<24
278	fourccXB4H = 'X' | 'B'<<8 | '4'<<16 | 'H'<<24
279)
280
281// build validates the planes and turns them into a buffer; ok is false after
282// a protocol error, failed means the client gets `failed`.
283func (p *params) build(width, height int32, format, flags uint32) (buf *dmabufBuffer, failed, ok bool) {
284	r := p.resource
285	if p.used {
286		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed), "params already used")
287		return nil, false, false
288	}
289	p.used = true
290	n := 0
291	for n < maxPlanes && p.planes[n] != nil {
292		n++
293	}
294	if n == 0 || slices.ContainsFunc(p.planes[n:], func(pl *plane) bool { return pl != nil }) {
295		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorIncomplete), "missing planes")
296		return nil, false, false
297	}
298	if width <= 0 || height <= 0 {
299		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "invalid dimensions")
300		return nil, false, false
301	}
302	// Unsupported formats, y-inverted or interlaced buffers: the client
303	// falls back to another format or to wl_shm.
304	want := ports.DMABufFormat{Format: format, Modifier: p.modifier}
305	if flags != 0 || !slices.Contains(p.global.support.Formats, want) {
306		return nil, true, true
307	}
308	// NV12/P010 are 4:2:0 two-plane images. The same modifier must
309	// describe both planes; each plane may have its own fd and offset.
310	yuv := isYUV(format)
311	wantPlanes := 1
312	if yuv {
313		wantPlanes = 2
314	}
315	if n != wantPlanes {
316		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorIncomplete), "wrong plane count")
317		return nil, false, false
318	}
319	if yuv && (width%2 != 0 || height%2 != 0) {
320		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "4:2:0 requires even dimensions")
321		return nil, false, false
322	}
323	for i, pl := range p.planes[:n] {
324		row, rows := uint64(width)*4, uint64(height)
325		if yuv {
326			bytesPerSample := uint64(1)
327			if format == fourccP010 {
328				bytesPerSample = 2
329			}
330			row = uint64(width) * bytesPerSample
331			if i == 1 {
332				rows /= 2
333			}
334		} else if format == fourccAB4H || format == fourccXB4H {
335			row = uint64(width) * 8
336		}
337		end := uint64(pl.offset) + uint64(pl.stride)*(rows-1) + row
338		size, err := pl.file.Seek(0, io.SeekEnd)
339		if uint64(pl.stride) < row || (err == nil && end > uint64(size)) {
340			r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds), "plane out of bounds")
341			return nil, false, false
342		}
343	}
344	p.global.nextID++
345	d := &ports.DMABuf{ID: p.global.nextID, Width: int(width), Height: int(height), Format: format, Modifier: p.modifier}
346	for _, pl := range p.planes[:n] {
347		d.Planes = append(d.Planes, ports.DMABufPlane{File: pl.file, Offset: pl.offset, Stride: pl.stride})
348	}
349	// The buffer owns the files now.
350	p.planes = [maxPlanes]*plane{}
351	return &dmabufBuffer{buf: d}, false, true
352}
353
354func (p *params) Create(r *linuxdmabuf.ZwpLinuxBufferParamsV1, width, height int32, format, flags uint32) {
355	b, failed, ok := p.build(width, height, format, flags)
356	if !ok {
357		return
358	}
359	if failed {
360		r.SendFailed()
361		return
362	}
363	// A server-side wl_buffer: id 0 asks libwayland to allocate one.
364	res, err := wayland.NewBuffer(r.Client(), 1, 0, b)
365	if err != nil {
366		b.close()
367		r.SendFailed()
368		return
369	}
370	p.global.server.addBuffer(res, b)
371	r.SendCreated(res)
372}
373
374func (p *params) CreateImmed(r *linuxdmabuf.ZwpLinuxBufferParamsV1, id uint32, width, height int32, format, flags uint32) {
375	b, failed, ok := p.build(width, height, format, flags)
376	if !ok {
377		return
378	}
379	if failed {
380		// Immediate creation has no failure event: the protocol says to
381		// raise invalid_wl_buffer.
382		r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidWlBuffer), "unsupported buffer")
383		return
384	}
385	res, err := wayland.NewBuffer(r.Client(), 1, id, b)
386	if err != nil {
387		b.close()
388		return
389	}
390	p.global.server.addBuffer(res, b)
391}
392
393func (*params) SetSamplingDevice(*linuxdmabuf.ZwpLinuxBufferParamsV1, []byte) {}
394
395// dmabufBuffer is a wl_buffer backed by client GPU memory.
396type dmabufBuffer struct{ buf *ports.DMABuf }
397
398func (*dmabufBuffer) Destroy(*wayland.Buffer) {}
399
400func (b *dmabufBuffer) close() {
401	for _, pl := range b.buf.Planes {
402		pl.File.Close()
403	}
404}
405
406func (b *dmabufBuffer) size() (int, int) { return b.buf.Width, b.buf.Height }
407
408// content hands the renderer the buffer itself: nothing is copied.
409func (b *dmabufBuffer) content(id ports.WindowID) (ports.SurfaceContent, bool) {
410	opaque := b.buf.Format == fourccXRGB || b.buf.Format == fourccXBGR || b.buf.Format == fourccXR30 || b.buf.Format == fourccXB30
411	return ports.SurfaceContent{ID: id, Width: b.buf.Width, Height: b.buf.Height, Opaque: opaque, DMABuf: b.buf}, true
412}
413
414const (
415	fourccXRGB = 'X' | 'R'<<8 | '2'<<16 | '4'<<24
416	fourccXBGR = 'X' | 'B'<<8 | '2'<<16 | '4'<<24
417	fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
418	fourccXB30 = 'X' | 'B'<<8 | '3'<<16 | '0'<<24
419)

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