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 := ¶ms{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)