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