v0.3.0 cf370a0

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

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