detached 1dd76b1

feat(wayland): advertise zwp_linux_dmabuf_v1 version 6 (#120)

* refactor(wayland): precompute dmabuf feedback tranches and track sent state

Compute the main device, renderer tranche indices and each output's scanout
tranche once instead of on every feedback send, using a format-to-index map
instead of a linear search. Feedback objects now carry their surface and
whether the format table was sent (no change in what is sent yet).

Behavior change: setOutputFormats now resends feedback only to surfaces
whose window is configured (hasLast), fullscreen on the output and visible,
like the scanout offer itself. Hidden or never-configured fullscreen
windows are no longer resent on format changes; they get their feedback on
their visible configure via resendSurface.

* feat(wayland): advertise zwp_linux_dmabuf_v1 version 6

Raise the global to version 6. From v6 the renderer tranche carries the
sampling flag, the format table is sent once per feedback object (it never
changes) and main_device is not sent, the tranche target devices naming
the device. v4 and v5 clients still get the table and main_device on every
round, with an unflagged renderer tranche.

params.set_sampling_device accepts an 8-byte dev_t (the single-device
renderer ignores it), and raises invalid_dev_t_size or already_used
otherwise. create and create_immed with a format and modifier that were
not advertised raise invalid_format for v4+ clients and keep failing for
older ones; non-zero flags still fail.
commit
1dd76b11641159a5c7946c46fd0e3d4fe73f993c
author
bnema · · 2026-10-03 20:13 UTC
committer
GitHub · · 2026-10-03 20:13 UTC
parent
41067c7
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +424 -118
M README.md +1 -1
@@ -53,7 +53,7 @@ The terminal command comes from `terminal`, then `$TERMINAL`, then `foot`. `term
5353
5454 ## The performance path
5555
56-- `zwp_linux_dmabuf_v1` v4: client buffers are sampled in place. Vulkan composes into exported scanout images and the frame fence goes to KMS as `IN_FENCE_FD`. No CPU copy, no CPU wait.
56+- `zwp_linux_dmabuf_v1` v6: client buffers are sampled in place. Vulkan composes into exported scanout images and the frame fence goes to KMS as `IN_FENCE_FD`. No CPU copy, no CPU wait.
5757 - Direct scanout of a fullscreen window's buffer; dmabuf feedback gives it a scanout-ready format first. A lone opaque window can use an overlay plane.
5858 - `wp_tearing_control_v1` and VRR while a fullscreen window covers the output.
5959 - `wp_linux_drm_syncobj_v1`: client fences go to KMS. NVIDIA drivers need it.
@@ -53,7 +53,7 @@ The terminal command comes from `terminal`, then `$TERMINAL`, then `foot`. `term
53 53
54 ## The performance path54 ## The performance path
55 55
56-- `zwp_linux_dmabuf_v1` v4: client buffers are sampled in place. Vulkan composes into exported scanout images and the frame fence goes to KMS as `IN_FENCE_FD`. No CPU copy, no CPU wait.56+- `zwp_linux_dmabuf_v1` v6: client buffers are sampled in place. Vulkan composes into exported scanout images and the frame fence goes to KMS as `IN_FENCE_FD`. No CPU copy, no CPU wait.
57 - Direct scanout of a fullscreen window's buffer; dmabuf feedback gives it a scanout-ready format first. A lone opaque window can use an overlay plane.57 - Direct scanout of a fullscreen window's buffer; dmabuf feedback gives it a scanout-ready format first. A lone opaque window can use an overlay plane.
58 - `wp_tearing_control_v1` and VRR while a fullscreen window covers the output.58 - `wp_tearing_control_v1` and VRR while a fullscreen window covers the output.
59 - `wp_linux_drm_syncobj_v1`: client fences go to KMS. NVIDIA drivers need it.59 - `wp_linux_drm_syncobj_v1`: client fences go to KMS. NVIDIA drivers need it.
M internal/adapters/wayland/dmabuf.go +139 -61
@@ -13,15 +13,20 @@ import (
1313 "golang.org/x/sys/unix"
1414 )
1515
16-// linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.
16+// linux-dmabuf v6: clients hand GPU buffers to the renderer without copies.
1717 // The formats are those the renderer imports (ports.DMABufSupport); without
1818 // any, the global is not advertised and clients use wl_shm. Surface
1919 // feedback also offers, first, a scanout tranche while the surface's
2020 // window is fullscreen on an output that reported scanout formats
2121 // (ports.OutputFormats): a buffer allocated from it can be flipped to the
22-// display with no composition.
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.
2328
24-const dmabufVersion = 4
29+const dmabufVersion = 6
2530
2631 // maxPlanes is the protocol limit (DRM planes).
2732 const maxPlanes = 4
@@ -34,7 +39,24 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error
3439 if err != nil {
3540 return err
3641 }
37- s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table, scanout: map[string]ports.OutputFormats{}, feedbacks: map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface{}}
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+ }
3860 return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
3961 r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
4062 if err != nil || v >= 4 {
@@ -81,11 +103,42 @@ type dmabufGlobal struct {
81103 support ports.DMABufSupport
82104 table *os.File
83105 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
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.
120+type 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.
128+type feedbackState struct {
129+ surf *surface
130+ tableSent bool
131+}
132+
133+// offer builds the scanout tranche of an output's formats.
134+func (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
89142 }
90143
91144 func (g *dmabufGlobal) close() {
@@ -121,9 +174,10 @@ func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf
121174 if err != nil {
122175 return
123176 }
124- g.feedbacks[fb] = surf
177+ st := &feedbackState{surf: surf}
178+ g.feedbacks[fb] = st
125179 fb.OnDestroy = func() { delete(g.feedbacks, fb) }
126- g.send(fb, surf)
180+ g.send(fb, st)
127181 }
128182
129183 // devBytes is a dev_t as the protocol's array.
@@ -133,49 +187,57 @@ func devBytes(dev uint64) []byte {
133187 return b
134188 }
135189
136-// send sends the whole feedback: table, main device, the scanout tranche
137-// when the surface has one, then the renderer tranche.
138-func (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)
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.
193+func (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)
163215 fb.SendTrancheDone()
164216 fb.SendDone()
165217 }
166218
167-// scanoutFor is the scanout offer for a surface: its window is fullscreen
168-// on an output that reported scanout formats.
169-func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {
219+// fullscreenOutput is the output a surface's window is visibly fullscreen
220+// on, once configured.
221+func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) {
170222 if surf == nil || surf.destroyed {
171- return ports.OutputFormats{}, false
223+ return "", false
172224 }
173225 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
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
176228 }
177- out, ok := g.scanout[root.xdg.window.last.Output]
178- return out, ok && len(out.Formats) > 0
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.
234+func (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
179241 }
180242
181243 // setOutputFormats records an output's scanout formats and sends the
@@ -183,19 +245,19 @@ func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {
183245 // formats the renderer samples (a refused buffer is composed), so they
184246 // are all in the table.
185247 func (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) {
248+ if old, ok := g.scanout[f.Output]; ok && old.formats.Device == f.Device && slices.Equal(old.formats.Formats, f.Formats) {
187249 return
188250 }
189251 if len(f.Formats) == 0 {
190252 delete(g.scanout, f.Output)
191253 } else {
192- g.scanout[f.Output] = f
254+ // Offers without table indices are kept, so repeated reports
255+ // stay deduplicated.
256+ g.scanout[f.Output] = g.offer(f)
193257 }
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- }
258+ for fb, st := range g.feedbacks {
259+ if out, ok := g.fullscreenOutput(st.surf); ok && out == f.Output {
260+ g.send(fb, st)
199261 }
200262 }
201263 }
@@ -206,10 +268,10 @@ func (g *dmabufGlobal) resendSurface(root *surface) {
206268 if g == nil {
207269 return
208270 }
209- for fb, surf := range g.feedbacks {
271+ for fb, st := range g.feedbacks {
210272 // Popups follow their toplevel's offer (scanoutFor).
211- if surf != nil && toplevelRoot(surf) == root {
212- g.send(fb, surf)
273+ if st.surf != nil && toplevelRoot(st.surf) == root {
274+ g.send(fb, st)
213275 }
214276 }
215277 }
@@ -299,10 +361,17 @@ func (p *params) build(width, height int32, format, flags uint32) (buf *dmabufBu
299361 r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "invalid dimensions")
300362 return nil, false, false
301363 }
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) {
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+ }
306375 return nil, true, true
307376 }
308377 // NV12/P010 are 4:2:0 two-plane images. The same modifier must
@@ -390,7 +459,16 @@ func (p *params) CreateImmed(r *linuxdmabuf.ZwpLinuxBufferParamsV1, id uint32, w
390459 p.global.server.addBuffer(res, b)
391460 }
392461
393-func (*params) SetSamplingDevice(*linuxdmabuf.ZwpLinuxBufferParamsV1, []byte) {}
462+// SetSamplingDevice accepts any dev_t: the renderer samples on a single
463+// device, and a hint for another one is not an error.
464+func (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+}
394472
395473 // dmabufBuffer is a wl_buffer backed by client GPU memory.
396474 type dmabufBuffer struct{ buf *ports.DMABuf }
@@ -13,15 +13,20 @@ import (
13 "golang.org/x/sys/unix"13 "golang.org/x/sys/unix"
14 )14 )
15 15
16-// linux-dmabuf v4: clients hand GPU buffers to the renderer without copies.16+// linux-dmabuf v6: clients hand GPU buffers to the renderer without copies.
17 // The formats are those the renderer imports (ports.DMABufSupport); without17 // The formats are those the renderer imports (ports.DMABufSupport); without
18 // any, the global is not advertised and clients use wl_shm. Surface18 // any, the global is not advertised and clients use wl_shm. Surface
19 // feedback also offers, first, a scanout tranche while the surface's19 // feedback also offers, first, a scanout tranche while the surface's
20 // window is fullscreen on an output that reported scanout formats20 // window is fullscreen on an output that reported scanout formats
21 // (ports.OutputFormats): a buffer allocated from it can be flipped to the21 // (ports.OutputFormats): a buffer allocated from it can be flipped to the
22-// display with no composition.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.
23 28
24-const dmabufVersion = 429+const dmabufVersion = 6
25 30
26 // maxPlanes is the protocol limit (DRM planes).31 // maxPlanes is the protocol limit (DRM planes).
27 const maxPlanes = 432 const maxPlanes = 4
@@ -34,7 +39,24 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error
34 if err != nil {39 if err != nil {
35 return err40 return err
36 }41 }
37- s.dmabuf = &dmabufGlobal{server: s, support: sup, table: table, scanout: map[string]ports.OutputFormats{}, feedbacks: map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface{}}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+ }
38 return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {60 return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) {
39 r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)61 r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf)
40 if err != nil || v >= 4 {62 if err != nil || v >= 4 {
@@ -81,11 +103,42 @@ type dmabufGlobal struct {
81 support ports.DMABufSupport103 support ports.DMABufSupport
82 table *os.File104 table *os.File
83 nextID uint64105 nextID uint64
84- // scanout are the outputs' direct scanout formats by output name.106+ // mainDev is the renderer device, and rendererIndices the u16 table
85- scanout map[string]ports.OutputFormats107+ // indices of every supported format: both are fixed for the server's
86- // feedbacks are the live feedback objects and their surface (nil:108+ // lifetime.
87- // default feedback).109+ mainDev []byte
88- feedbacks map[*linuxdmabuf.ZwpLinuxDmabufFeedbackV1]*surface110+ 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.
120+type 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.
128+type feedbackState struct {
129+ surf *surface
130+ tableSent bool
131+}
132+
133+// offer builds the scanout tranche of an output's formats.
134+func (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
89 }142 }
90 143
91 func (g *dmabufGlobal) close() {144 func (g *dmabufGlobal) close() {
@@ -121,9 +174,10 @@ func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf
121 if err != nil {174 if err != nil {
122 return175 return
123 }176 }
124- g.feedbacks[fb] = surf177+ st := &feedbackState{surf: surf}
178+ g.feedbacks[fb] = st
125 fb.OnDestroy = func() { delete(g.feedbacks, fb) }179 fb.OnDestroy = func() { delete(g.feedbacks, fb) }
126- g.send(fb, surf)180+ g.send(fb, st)
127 }181 }
128 182
129 // devBytes is a dev_t as the protocol's array.183 // devBytes is a dev_t as the protocol's array.
@@ -133,49 +187,57 @@ func devBytes(dev uint64) []byte {
133 return b187 return b
134 }188 }
135 189
136-// send sends the whole feedback: table, main device, the scanout tranche190+// send sends a feedback round: the table (once from v6), the main device
137-// when the surface has one, then the renderer tranche.191+// (before v6), the scanout tranche when the surface has one, then the
138-func (g *dmabufGlobal) send(fb *linuxdmabuf.ZwpLinuxDmabufFeedbackV1, surf *surface) {192+// renderer tranche.
139- fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))193+func (g *dmabufGlobal) send(fb *linuxdmabuf.ZwpLinuxDmabufFeedbackV1, st *feedbackState) {
140- main := devBytes(g.support.Device)194+ v6 := fb.Version() >= 6
141- fb.SendMainDevice(main)195+ if !st.tableSent || !v6 {
142- if out, ok := g.scanoutFor(surf); ok {196+ fb.SendFormatTable(int(g.table.Fd()), uint32(16*len(g.support.Formats)))
143- var indices []byte197+ st.tableSent = true
144- for _, f := range out.Formats {198+ }
145- if i := slices.Index(g.support.Formats, f); i >= 0 {199+ if !v6 {
146- indices = binary.NativeEndian.AppendUint16(indices, uint16(i))200+ fb.SendMainDevice(g.mainDev)
147- }201+ }
148- }202+ if offer, ok := g.scanoutFor(st.surf); ok && len(offer.indices) > 0 {
149- if len(indices) > 0 {203+ fb.SendTrancheTargetDevice(offer.device)
150- fb.SendTrancheTargetDevice(devBytes(out.Device))204+ fb.SendTrancheFlags(uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout))
151- fb.SendTrancheFlags(uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout))205+ fb.SendTrancheFormats(offer.indices)
152- fb.SendTrancheFormats(indices)206+ fb.SendTrancheDone()
153- fb.SendTrancheDone()207+ }
154- }208+ fb.SendTrancheTargetDevice(g.mainDev)
155- }209+ rendererFlags := uint32(0)
156- indices := make([]byte, 2*len(g.support.Formats))210+ if v6 {
157- for i := range g.support.Formats {211+ rendererFlags = uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsSampling)
158- binary.NativeEndian.PutUint16(indices[i*2:], uint16(i))212+ }
159- }213+ fb.SendTrancheFlags(rendererFlags)
160- fb.SendTrancheTargetDevice(main)214+ fb.SendTrancheFormats(g.rendererIndices)
161- fb.SendTrancheFlags(0)
162- fb.SendTrancheFormats(indices)
163 fb.SendTrancheDone()215 fb.SendTrancheDone()
164 fb.SendDone()216 fb.SendDone()
165 }217 }
166 218
167-// scanoutFor is the scanout offer for a surface: its window is fullscreen219+// fullscreenOutput is the output a surface's window is visibly fullscreen
168-// on an output that reported scanout formats.220+// on, once configured.
169-func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {221+func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) {
170 if surf == nil || surf.destroyed {222 if surf == nil || surf.destroyed {
171- return ports.OutputFormats{}, false223+ return "", false
172 }224 }
173 root := toplevelRoot(surf)225 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 {226+ if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) {
175- return ports.OutputFormats{}, false227+ return "", false
176 }228 }
177- out, ok := g.scanout[root.xdg.window.last.Output]229+ return root.xdg.window.last.Output, true
178- return out, ok && len(out.Formats) > 0230+}
231+
232+// scanoutFor is the scanout offer for a surface: its window is fullscreen
233+// on an output that reported scanout formats.
234+func (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
179 }241 }
180 242
181 // setOutputFormats records an output's scanout formats and sends the243 // setOutputFormats records an output's scanout formats and sends the
@@ -183,19 +245,19 @@ func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) {
183 // formats the renderer samples (a refused buffer is composed), so they245 // formats the renderer samples (a refused buffer is composed), so they
184 // are all in the table.246 // are all in the table.
185 func (g *dmabufGlobal) setOutputFormats(f ports.OutputFormats) {247 func (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) {248+ if old, ok := g.scanout[f.Output]; ok && old.formats.Device == f.Device && slices.Equal(old.formats.Formats, f.Formats) {
187 return249 return
188 }250 }
189 if len(f.Formats) == 0 {251 if len(f.Formats) == 0 {
190 delete(g.scanout, f.Output)252 delete(g.scanout, f.Output)
191 } else {253 } else {
192- g.scanout[f.Output] = f254+ // Offers without table indices are kept, so repeated reports
255+ // stay deduplicated.
256+ g.scanout[f.Output] = g.offer(f)
193 }257 }
194- for fb, surf := range g.feedbacks {258+ for fb, st := range g.feedbacks {
195- if surf != nil && !surf.destroyed {259+ if out, ok := g.fullscreenOutput(st.surf); ok && out == f.Output {
196- if root := toplevelRoot(surf); root.xdg != nil && root.xdg.window != nil && root.xdg.window.last.Fullscreen && root.xdg.window.last.Output == f.Output {260+ g.send(fb, st)
197- g.send(fb, surf)
198- }
199 }261 }
200 }262 }
201 }263 }
@@ -206,10 +268,10 @@ func (g *dmabufGlobal) resendSurface(root *surface) {
206 if g == nil {268 if g == nil {
207 return269 return
208 }270 }
209- for fb, surf := range g.feedbacks {271+ for fb, st := range g.feedbacks {
210 // Popups follow their toplevel's offer (scanoutFor).272 // Popups follow their toplevel's offer (scanoutFor).
211- if surf != nil && toplevelRoot(surf) == root {273+ if st.surf != nil && toplevelRoot(st.surf) == root {
212- g.send(fb, surf)274+ g.send(fb, st)
213 }275 }
214 }276 }
215 }277 }
@@ -299,10 +361,17 @@ func (p *params) build(width, height int32, format, flags uint32) (buf *dmabufBu
299 r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "invalid dimensions")361 r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "invalid dimensions")
300 return nil, false, false362 return nil, false, false
301 }363 }
302- // Unsupported formats, y-inverted or interlaced buffers: the client364+ // Y-inverted or interlaced buffers fail: the client falls back to
303- // falls back to another format or to wl_shm.365+ // another format or to wl_shm. An unadvertised format+modifier is
304- want := ports.DMABufFormat{Format: format, Modifier: p.modifier}366+ // invalid_format from v4, a failure before.
305- if flags != 0 || !slices.Contains(p.global.support.Formats, want) {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+ }
306 return nil, true, true375 return nil, true, true
307 }376 }
308 // NV12/P010 are 4:2:0 two-plane images. The same modifier must377 // NV12/P010 are 4:2:0 two-plane images. The same modifier must
@@ -390,7 +459,16 @@ func (p *params) CreateImmed(r *linuxdmabuf.ZwpLinuxBufferParamsV1, id uint32, w
390 p.global.server.addBuffer(res, b)459 p.global.server.addBuffer(res, b)
391 }460 }
392 461
393-func (*params) SetSamplingDevice(*linuxdmabuf.ZwpLinuxBufferParamsV1, []byte) {}462+// SetSamplingDevice accepts any dev_t: the renderer samples on a single
463+// device, and a hint for another one is not an error.
464+func (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+}
394 472
395 // dmabufBuffer is a wl_buffer backed by client GPU memory.473 // dmabufBuffer is a wl_buffer backed by client GPU memory.
396 type dmabufBuffer struct{ buf *ports.DMABuf }474 type dmabufBuffer struct{ buf *ports.DMABuf }
M internal/adapters/wayland/dmabuf_test.go +284 -56
@@ -57,8 +57,8 @@ type paramsProxy struct {
5757
5858 func (p *paramsProxy) Dispatch(e *wlturbo.Event) { p.result <- e.Opcode }
5959
60-// A format the renderer did not list fails; a plane past the end of its
61-// file is a protocol error.
60+// An unadvertised format is a protocol error from v4 and `failed` before; a
61+// plane past the end of its file is a protocol error.
6262 func TestDMABufParams(t *testing.T) {
6363 s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
6464 c := protocolClient(t, s, dir)
@@ -93,13 +93,8 @@ func TestDMABufParams(t *testing.T) {
9393 }
9494 return p, proxy.result
9595 }
96- // NV12 is not in the list: failed, not a protocol error.
97- _, result := create('N'|'V'<<8|'1'<<16|'2'<<24, 16)
98- if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
99- t.Fatalf("event %d", op)
100- }
10196 // Supported: created (import errors surface in the renderer).
102- _, result = create(linearARGB.Format, 16)
97+ _, result := create(linearARGB.Format, 16)
10398 if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
10499 t.Fatalf("event %d", op)
105100 }
@@ -111,6 +106,132 @@ func TestDMABufParams(t *testing.T) {
111106 }
112107 requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0))
113108 expectProtocolError(t, c, big, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds))
109+ // Each case below needs its own connection: a protocol error kills it.
110+ t.Run("unadvertised format v6", func(t *testing.T) {
111+ c, p, _ := createNV12(t, dmabufVersion, false, 0)
112+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat))
113+ })
114+ t.Run("unadvertised format v6 immed", func(t *testing.T) {
115+ c, p, _ := createNV12(t, dmabufVersion, true, 0)
116+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat))
117+ })
118+ // Flags are checked first: a y-inverted buffer fails, whatever its format.
119+ t.Run("flags before format v6", func(t *testing.T) {
120+ c, _, proxy := createNV12(t, dmabufVersion, false, 1)
121+ if err := c.Roundtrip(); err != nil {
122+ t.Fatal(err)
123+ }
124+ if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
125+ t.Fatalf("event %d", op)
126+ }
127+ })
128+ // Before v4 there is no feedback to follow: the client gets `failed`,
129+ // or invalid_wl_buffer for an immediate create.
130+ t.Run("unadvertised format v3 immed", func(t *testing.T) {
131+ c, p, _ := createNV12(t, 3, true, 0)
132+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidWlBuffer))
133+ })
134+ t.Run("unadvertised format v3", func(t *testing.T) {
135+ c, _, proxy := createNV12(t, 3, false, 0)
136+ if err := c.Roundtrip(); err != nil {
137+ t.Fatal(err)
138+ }
139+ if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
140+ t.Fatalf("event %d", op)
141+ }
142+ })
143+}
144+
145+// createNV12 sends a one-plane create (or create_immed) of NV12, which the
146+// server does not list, on a fresh connection bound at the given version.
147+func createNV12(t *testing.T, version uint32, immed bool, flags uint32) (*wlturbo.Display, uint32, *paramsProxy) {
148+ t.Helper()
149+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
150+ c := protocolClient(t, s, dir)
151+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", version)
152+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
153+ if err != nil {
154+ t.Fatal(err)
155+ }
156+ t.Cleanup(func() { unix.Close(fd) })
157+ if err := unix.Ftruncate(fd, 256*16); err != nil {
158+ t.Fatal(err)
159+ }
160+ p := c.AllocateID()
161+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
162+ proxy := &paramsProxy{result: make(chan uint16, 2)}
163+ proxy.SetID(p)
164+ registerWireProxy(c, proxy)
165+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
166+ t.Fatal(err)
167+ }
168+ if immed {
169+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, c.AllocateID(), int32(64), int32(16), uint32(fourccNV12), flags)
170+ } else {
171+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), uint32(fourccNV12), flags)
172+ }
173+ return c, p, proxy
174+}
175+
176+// set_sampling_device takes an 8-byte dev_t, before the params are used.
177+func TestDMABufSamplingDevice(t *testing.T) {
178+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
179+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
180+ if err != nil {
181+ t.Fatal(err)
182+ }
183+ defer unix.Close(fd)
184+ if err := unix.Ftruncate(fd, 256*16); err != nil {
185+ t.Fatal(err)
186+ }
187+ newParams := func(c *wlturbo.Display, dm uint32) (uint32, *paramsProxy) {
188+ p := c.AllocateID()
189+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
190+ proxy := &paramsProxy{result: make(chan uint16, 2)}
191+ proxy.SetID(p)
192+ registerWireProxy(c, proxy)
193+ return p, proxy
194+ }
195+ setDevice := func(c *wlturbo.Display, p uint32, dev []byte) {
196+ t.Helper()
197+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestSetSamplingDevice), nil, dev); err != nil {
198+ t.Fatal(err)
199+ }
200+ }
201+ t.Run("accepted", func(t *testing.T) {
202+ c := protocolClient(t, s, dir)
203+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
204+ p, proxy := newParams(c, dm)
205+ setDevice(c, p, devBytes(42))
206+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
207+ t.Fatal(err)
208+ }
209+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
210+ if err := c.Roundtrip(); err != nil {
211+ t.Fatal(err)
212+ }
213+ if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
214+ t.Fatalf("event %d", op)
215+ }
216+ })
217+ t.Run("bad size", func(t *testing.T) {
218+ c := protocolClient(t, s, dir)
219+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
220+ p, _ := newParams(c, dm)
221+ setDevice(c, p, make([]byte, 4))
222+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDevTSize))
223+ })
224+ t.Run("after create", func(t *testing.T) {
225+ c := protocolClient(t, s, dir)
226+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
227+ p, _ := newParams(c, dm)
228+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
229+ t.Fatal(err)
230+ }
231+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
232+ setDevice(c, p, devBytes(42))
233+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed))
234+ })
114235 }
115236
116237 // A reused params object is a protocol error.
@@ -173,14 +294,21 @@ func TestWestonDMABuf(t *testing.T) {
173294 }
174295 }
175296
176-// feedbackProxy records the tranches of each feedback round (done).
297+// feedbackProxy records each feedback round (up to done).
177298 type feedbackProxy struct {
178299 wlturbo.BaseProxy
179- rounds chan []tranche
180- cur []tranche
300+ rounds chan round
301+ cur round
181302 open tranche
182303 }
183304
305+// round is what one feedback round carried.
306+type round struct {
307+ tranches []tranche
308+ tables int
309+ mainDevices int
310+}
311+
184312 type tranche struct {
185313 flags uint32
186314 indices []uint16
@@ -189,9 +317,12 @@ type tranche struct {
189317 func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
190318 switch uint32(e.Opcode) {
191319 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:
320+ p.cur.tables++
192321 if fd := e.Fd(); fd != 0 {
193322 unix.Close(int(fd))
194323 }
324+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventMainDevice:
325+ p.cur.mainDevices++
195326 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:
196327 p.open.flags = e.Uint32()
197328 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:
@@ -200,86 +331,115 @@ func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
200331 p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)
201332 }
202333 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:
203- p.cur, p.open = append(p.cur, p.open), tranche{}
334+ p.cur.tranches, p.open = append(p.cur.tranches, p.open), tranche{}
204335 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:
205336 p.rounds <- p.cur
206- p.cur = nil
337+ p.cur = round{}
207338 }
208339 }
209340
210-// A fullscreen surface on an output with scanout formats gets a scanout
211-// tranche first; leaving fullscreen sends the renderer tranche alone.
212-func TestDMABufScanoutTranche(t *testing.T) {
213- tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
214- sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
341+// scanoutHarness is a server with commands and output formats channels, a
342+// mapped window, and a surface feedback bound at the given version.
343+type scanoutHarness struct {
344+ t *testing.T
345+ c *wlturbo.Display
346+ s *Server
347+ w ports.WindowMapped
348+ fb uint32
349+ proxy *feedbackProxy
350+ commands chan ports.ClientCommand
351+ formats chan ports.OutputFormats
352+}
353+
354+func newScanoutHarness(t *testing.T, sup ports.DMABufSupport, version uint32) *scanoutHarness {
355+ t.Helper()
215356 dir := t.TempDir()
216357 events := make(chan ports.ClientEvent, 16)
217- commands := make(chan ports.ClientCommand, 16)
218- formats := make(chan ports.OutputFormats, 1)
219- s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: commands, OutputFormats: formats}, logging.For(context.Background(), "wayland"))
358+ h := &scanoutHarness{t: t, commands: make(chan ports.ClientCommand, 16), formats: make(chan ports.OutputFormats, 1)}
359+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: h.commands, OutputFormats: h.formats}, logging.For(context.Background(), "wayland"))
220360 if err != nil {
221361 t.Fatal(err)
222362 }
363+ h.s = s
223364 ctx, cancel := context.WithCancel(context.Background())
224365 done := make(chan error, 1)
225366 go func() { done <- s.Run(ctx) }()
226367 t.Cleanup(func() { cancel(); <-done })
227- c := protocolClient(t, s, dir)
228- w, surf, xdg := surfaceMapper(t, c, events)()
368+ h.c = protocolClient(t, s, dir)
369+ w, surf, xdg := surfaceMapper(t, h.c, events)()
370+ h.w = w
229371 // Our configures must not block the mapper's one-slot proxy.
230- registerProtocol(t, c, xdg)
231- dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
232- fb := c.AllocateID()
233- proxy := &feedbackProxy{rounds: make(chan []tranche, 8)}
234- proxy.SetID(fb)
235- registerWireProxy(c, proxy)
236- requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, fb, surf)
237- round := func(what string) []tranche {
238- t.Helper()
239- deadline := time.After(2 * time.Second)
240- for {
241- if err := c.Roundtrip(); err != nil {
242- t.Fatal(err)
243- }
244- select {
245- case r := <-proxy.rounds:
246- return r
247- case <-deadline:
248- t.Fatalf("no feedback: %s", what)
249- default:
250- time.Sleep(5 * time.Millisecond)
251- }
372+ registerProtocol(t, h.c, xdg)
373+ dm := bindVersion(t, h.c, "zwp_linux_dmabuf_v1", version)
374+ h.fb = h.c.AllocateID()
375+ h.proxy = &feedbackProxy{rounds: make(chan round, 8)}
376+ h.proxy.SetID(h.fb)
377+ registerWireProxy(h.c, h.proxy)
378+ requestProtocol(t, h.c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, h.fb, surf)
379+ return h
380+}
381+
382+// next waits for the next feedback round.
383+func (h *scanoutHarness) next(what string) round {
384+ h.t.Helper()
385+ deadline := time.After(2 * time.Second)
386+ for {
387+ if err := h.c.Roundtrip(); err != nil {
388+ h.t.Fatal(err)
389+ }
390+ select {
391+ case r := <-h.proxy.rounds:
392+ return r
393+ case <-deadline:
394+ h.t.Fatalf("no feedback: %s", what)
395+ default:
396+ time.Sleep(5 * time.Millisecond)
252397 }
253398 }
399+}
400+
401+// A fullscreen surface on an output with scanout formats gets a scanout
402+// tranche first; leaving fullscreen sends the renderer tranche alone. From
403+// v6 the table comes once, main_device never, and the renderer tranche is
404+// flagged for sampling.
405+func TestDMABufScanoutTranche(t *testing.T) {
406+ tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
407+ sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
408+ h := newScanoutHarness(t, sup, dmabufVersion)
409+ c, s, w, fb, commands, formats := h.c, h.s, h.w, h.fb, h.commands, h.formats
254410 scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)
255- if r := round("initial"); len(r) != 1 || r[0].flags != 0 || len(r[0].indices) != 2 {
411+ sampling := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsSampling)
412+ if r := h.next("initial"); len(r.tranches) != 1 || r.tranches[0].flags != sampling || len(r.tranches[0].indices) != 2 || r.tables != 1 || r.mainDevices != 0 {
256413 t.Fatalf("tiled window: %+v", r)
257414 }
258415 formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
259416 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
260- r := round("fullscreen")
261- for len(r) == 1 { // the formats may land after the configure
262- r = round("fullscreen with formats")
417+ r := h.next("fullscreen")
418+ for len(r.tranches) == 1 { // the formats may land after the configure
419+ if r.tables != 0 || r.mainDevices != 0 {
420+ t.Fatalf("resent table or main device: %+v", r)
421+ }
422+ r = h.next("fullscreen with formats")
263423 }
264- if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 {
424+ if len(r.tranches) != 2 || r.tranches[0].flags != scanout || len(r.tranches[0].indices) != 1 || r.tranches[0].indices[0] != 1 || r.tranches[1].flags != sampling || r.tables != 0 || r.mainDevices != 0 {
265425 t.Fatalf("fullscreen: %+v", r)
266426 }
267427 // Hidden, the window loses its scanout tranche; shown, it gets it back.
268428 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
269- if r := round("hidden"); len(r) != 1 {
429+ if r := h.next("hidden"); len(r.tranches) != 1 || r.tables != 0 {
270430 t.Fatalf("hidden fullscreen: %+v", r)
271431 }
272432 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
273- if r := round("shown again"); len(r) != 2 || r[0].flags != scanout {
433+ if r := h.next("shown again"); len(r.tranches) != 2 || r.tranches[0].flags != scanout || r.tables != 0 {
274434 t.Fatalf("shown fullscreen: %+v", r)
275435 }
276436 // Fullscreen on another output: its formats (none) apply at once.
277437 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1", Visible: true}
278- if r := round("other output"); len(r) != 1 {
438+ if r := h.next("other output"); len(r.tranches) != 1 || r.tables != 0 {
279439 t.Fatalf("moved output: %+v", r)
280440 }
281441 commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1", Visible: true}
282- if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 {
442+ if r := h.next("tiled again"); len(r.tranches) != 1 || r.tranches[0].flags != sampling || r.tables != 0 {
283443 t.Fatalf("left fullscreen: %+v", r)
284444 }
285445 // A destroyed feedback gets nothing more (a protocol error would
@@ -297,11 +457,79 @@ func TestDMABufScanoutTranche(t *testing.T) {
297457 }
298458 }
299459
460+// A v4 client keeps the whole feedback on every round: table, main device,
461+// and an unflagged renderer tranche.
462+func TestDMABufFeedbackV4(t *testing.T) {
463+ tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
464+ sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
465+ h := newScanoutHarness(t, sup, 4)
466+ check := func(what string, r round, tranches int) {
467+ t.Helper()
468+ if r.tables != 1 || r.mainDevices != 1 || len(r.tranches) != tranches || r.tranches[len(r.tranches)-1].flags != 0 {
469+ t.Fatalf("%s: %+v", what, r)
470+ }
471+ }
472+ check("initial", h.next("initial"), 1)
473+ h.formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
474+ h.commands <- ports.ConfigureWindow{ID: h.w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
475+ r := h.next("fullscreen")
476+ for len(r.tranches) == 1 { // the formats may land after the configure
477+ check("fullscreen before formats", r, 1)
478+ r = h.next("fullscreen with formats")
479+ }
480+ check("fullscreen", r, 2)
481+ if r.tranches[0].flags != uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout) {
482+ t.Fatalf("scanout tranche: %+v", r)
483+ }
484+}
485+
486+// testDMABufGlobal is a global with one scanout offer for f's output.
487+func testDMABufGlobal(s *Server, f ports.OutputFormats) *dmabufGlobal {
488+ g := &dmabufGlobal{
489+ server: s,
490+ support: ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}},
491+ index: map[ports.DMABufFormat]uint16{linearARGB: 0},
492+ scanout: map[string]scanoutOffer{},
493+ }
494+ g.scanout[f.Output] = g.offer(f)
495+ return g
496+}
497+
498+func TestFullscreenOutput(t *testing.T) {
499+ windowSurface := func(mod func(*window)) *surface {
500+ w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
501+ mod(w)
502+ return &surface{xdg: &xdgSurface{window: w}}
503+ }
504+ tests := []struct {
505+ name string
506+ surf *surface
507+ out string
508+ ok bool
509+ }{
510+ {"nil", nil, "", false},
511+ {"destroyed", &surface{destroyed: true, xdg: &xdgSurface{window: &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Visible: true}}}}, "", false},
512+ {"no window", &surface{xdg: &xdgSurface{}}, "", false},
513+ {"not configured", windowSurface(func(w *window) { w.hasLast = false }), "", false},
514+ {"not fullscreen", windowSurface(func(w *window) { w.last.Fullscreen = false }), "", false},
515+ {"invisible", windowSurface(func(w *window) { w.last.Visible = false }), "", false},
516+ {"visible fullscreen", windowSurface(func(*window) {}), "DP-2", true},
517+ }
518+ g := testDMABufGlobal(&Server{}, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
519+ for _, tc := range tests {
520+ t.Run(tc.name, func(t *testing.T) {
521+ if out, ok := g.fullscreenOutput(tc.surf); out != tc.out || ok != tc.ok {
522+ t.Fatalf("got (%q, %v), want (%q, %v)", out, ok, tc.out, tc.ok)
523+ }
524+ })
525+ }
526+}
527+
300528 func TestInvisibleScanoutOffer(t *testing.T) {
301529 w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
302530 surf := &surface{xdg: &xdgSurface{window: w}}
303531 s := &Server{}
304- g := &dmabufGlobal{server: s, scanout: map[string]ports.OutputFormats{"DP-2": {Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}}}}
532+ g := testDMABufGlobal(s, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
305533 if _, ok := g.scanoutFor(surf); !ok {
306534 t.Fatal("no visible scanout")
307535 }
@@ -330,7 +558,7 @@ func TestPopupFeedbackFollowsToplevel(t *testing.T) {
330558 if toplevelRoot(pop) != top {
331559 t.Fatal("popup does not resolve to its toplevel")
332560 }
333- g := &dmabufGlobal{server: &Server{}, scanout: map[string]ports.OutputFormats{"DP-2": {Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}}}}
561+ g := testDMABufGlobal(&Server{}, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
334562 if _, ok := g.scanoutFor(pop); !ok {
335563 t.Fatal("popup of a visible fullscreen window has no offer")
336564 }
@@ -57,8 +57,8 @@ type paramsProxy struct {
57 57
58 func (p *paramsProxy) Dispatch(e *wlturbo.Event) { p.result <- e.Opcode }58 func (p *paramsProxy) Dispatch(e *wlturbo.Event) { p.result <- e.Opcode }
59 59
60-// A format the renderer did not list fails; a plane past the end of its60+// An unadvertised format is a protocol error from v4 and `failed` before; a
61-// file is a protocol error.61+// plane past the end of its file is a protocol error.
62 func TestDMABufParams(t *testing.T) {62 func TestDMABufParams(t *testing.T) {
63 s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})63 s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
64 c := protocolClient(t, s, dir)64 c := protocolClient(t, s, dir)
@@ -93,13 +93,8 @@ func TestDMABufParams(t *testing.T) {
93 }93 }
94 return p, proxy.result94 return p, proxy.result
95 }95 }
96- // NV12 is not in the list: failed, not a protocol error.
97- _, result := create('N'|'V'<<8|'1'<<16|'2'<<24, 16)
98- if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
99- t.Fatalf("event %d", op)
100- }
101 // Supported: created (import errors surface in the renderer).96 // Supported: created (import errors surface in the renderer).
102- _, result = create(linearARGB.Format, 16)97+ _, result := create(linearARGB.Format, 16)
103 if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {98 if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
104 t.Fatalf("event %d", op)99 t.Fatalf("event %d", op)
105 }100 }
@@ -111,6 +106,132 @@ func TestDMABufParams(t *testing.T) {
111 }106 }
112 requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0))107 requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0))
113 expectProtocolError(t, c, big, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds))108 expectProtocolError(t, c, big, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds))
109+ // Each case below needs its own connection: a protocol error kills it.
110+ t.Run("unadvertised format v6", func(t *testing.T) {
111+ c, p, _ := createNV12(t, dmabufVersion, false, 0)
112+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat))
113+ })
114+ t.Run("unadvertised format v6 immed", func(t *testing.T) {
115+ c, p, _ := createNV12(t, dmabufVersion, true, 0)
116+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat))
117+ })
118+ // Flags are checked first: a y-inverted buffer fails, whatever its format.
119+ t.Run("flags before format v6", func(t *testing.T) {
120+ c, _, proxy := createNV12(t, dmabufVersion, false, 1)
121+ if err := c.Roundtrip(); err != nil {
122+ t.Fatal(err)
123+ }
124+ if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
125+ t.Fatalf("event %d", op)
126+ }
127+ })
128+ // Before v4 there is no feedback to follow: the client gets `failed`,
129+ // or invalid_wl_buffer for an immediate create.
130+ t.Run("unadvertised format v3 immed", func(t *testing.T) {
131+ c, p, _ := createNV12(t, 3, true, 0)
132+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidWlBuffer))
133+ })
134+ t.Run("unadvertised format v3", func(t *testing.T) {
135+ c, _, proxy := createNV12(t, 3, false, 0)
136+ if err := c.Roundtrip(); err != nil {
137+ t.Fatal(err)
138+ }
139+ if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
140+ t.Fatalf("event %d", op)
141+ }
142+ })
143+}
144+
145+// createNV12 sends a one-plane create (or create_immed) of NV12, which the
146+// server does not list, on a fresh connection bound at the given version.
147+func createNV12(t *testing.T, version uint32, immed bool, flags uint32) (*wlturbo.Display, uint32, *paramsProxy) {
148+ t.Helper()
149+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
150+ c := protocolClient(t, s, dir)
151+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", version)
152+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
153+ if err != nil {
154+ t.Fatal(err)
155+ }
156+ t.Cleanup(func() { unix.Close(fd) })
157+ if err := unix.Ftruncate(fd, 256*16); err != nil {
158+ t.Fatal(err)
159+ }
160+ p := c.AllocateID()
161+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
162+ proxy := &paramsProxy{result: make(chan uint16, 2)}
163+ proxy.SetID(p)
164+ registerWireProxy(c, proxy)
165+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
166+ t.Fatal(err)
167+ }
168+ if immed {
169+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, c.AllocateID(), int32(64), int32(16), uint32(fourccNV12), flags)
170+ } else {
171+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), uint32(fourccNV12), flags)
172+ }
173+ return c, p, proxy
174+}
175+
176+// set_sampling_device takes an 8-byte dev_t, before the params are used.
177+func TestDMABufSamplingDevice(t *testing.T) {
178+ s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
179+ fd, err := unix.MemfdCreate("fake-dmabuf", 0)
180+ if err != nil {
181+ t.Fatal(err)
182+ }
183+ defer unix.Close(fd)
184+ if err := unix.Ftruncate(fd, 256*16); err != nil {
185+ t.Fatal(err)
186+ }
187+ newParams := func(c *wlturbo.Display, dm uint32) (uint32, *paramsProxy) {
188+ p := c.AllocateID()
189+ requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
190+ proxy := &paramsProxy{result: make(chan uint16, 2)}
191+ proxy.SetID(p)
192+ registerWireProxy(c, proxy)
193+ return p, proxy
194+ }
195+ setDevice := func(c *wlturbo.Display, p uint32, dev []byte) {
196+ t.Helper()
197+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestSetSamplingDevice), nil, dev); err != nil {
198+ t.Fatal(err)
199+ }
200+ }
201+ t.Run("accepted", func(t *testing.T) {
202+ c := protocolClient(t, s, dir)
203+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
204+ p, proxy := newParams(c, dm)
205+ setDevice(c, p, devBytes(42))
206+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
207+ t.Fatal(err)
208+ }
209+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
210+ if err := c.Roundtrip(); err != nil {
211+ t.Fatal(err)
212+ }
213+ if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
214+ t.Fatalf("event %d", op)
215+ }
216+ })
217+ t.Run("bad size", func(t *testing.T) {
218+ c := protocolClient(t, s, dir)
219+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
220+ p, _ := newParams(c, dm)
221+ setDevice(c, p, make([]byte, 4))
222+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDevTSize))
223+ })
224+ t.Run("after create", func(t *testing.T) {
225+ c := protocolClient(t, s, dir)
226+ dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
227+ p, _ := newParams(c, dm)
228+ if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
229+ t.Fatal(err)
230+ }
231+ requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
232+ setDevice(c, p, devBytes(42))
233+ expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed))
234+ })
114 }235 }
115 236
116 // A reused params object is a protocol error.237 // A reused params object is a protocol error.
@@ -173,14 +294,21 @@ func TestWestonDMABuf(t *testing.T) {
173 }294 }
174 }295 }
175 296
176-// feedbackProxy records the tranches of each feedback round (done).297+// feedbackProxy records each feedback round (up to done).
177 type feedbackProxy struct {298 type feedbackProxy struct {
178 wlturbo.BaseProxy299 wlturbo.BaseProxy
179- rounds chan []tranche300+ rounds chan round
180- cur []tranche301+ cur round
181 open tranche302 open tranche
182 }303 }
183 304
305+// round is what one feedback round carried.
306+type round struct {
307+ tranches []tranche
308+ tables int
309+ mainDevices int
310+}
311+
184 type tranche struct {312 type tranche struct {
185 flags uint32313 flags uint32
186 indices []uint16314 indices []uint16
@@ -189,9 +317,12 @@ type tranche struct {
189 func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {317 func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
190 switch uint32(e.Opcode) {318 switch uint32(e.Opcode) {
191 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:319 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:
320+ p.cur.tables++
192 if fd := e.Fd(); fd != 0 {321 if fd := e.Fd(); fd != 0 {
193 unix.Close(int(fd))322 unix.Close(int(fd))
194 }323 }
324+ case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventMainDevice:
325+ p.cur.mainDevices++
195 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:326 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:
196 p.open.flags = e.Uint32()327 p.open.flags = e.Uint32()
197 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:328 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:
@@ -200,86 +331,115 @@ func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
200 p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)331 p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)
201 }332 }
202 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:333 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:
203- p.cur, p.open = append(p.cur, p.open), tranche{}334+ p.cur.tranches, p.open = append(p.cur.tranches, p.open), tranche{}
204 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:335 case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:
205 p.rounds <- p.cur336 p.rounds <- p.cur
206- p.cur = nil337+ p.cur = round{}
207 }338 }
208 }339 }
209 340
210-// A fullscreen surface on an output with scanout formats gets a scanout341+// scanoutHarness is a server with commands and output formats channels, a
211-// tranche first; leaving fullscreen sends the renderer tranche alone.342+// mapped window, and a surface feedback bound at the given version.
212-func TestDMABufScanoutTranche(t *testing.T) {343+type scanoutHarness struct {
213- tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}344+ t *testing.T
214- sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}345+ c *wlturbo.Display
346+ s *Server
347+ w ports.WindowMapped
348+ fb uint32
349+ proxy *feedbackProxy
350+ commands chan ports.ClientCommand
351+ formats chan ports.OutputFormats
352+}
353+
354+func newScanoutHarness(t *testing.T, sup ports.DMABufSupport, version uint32) *scanoutHarness {
355+ t.Helper()
215 dir := t.TempDir()356 dir := t.TempDir()
216 events := make(chan ports.ClientEvent, 16)357 events := make(chan ports.ClientEvent, 16)
217- commands := make(chan ports.ClientCommand, 16)358+ h := &scanoutHarness{t: t, commands: make(chan ports.ClientCommand, 16), formats: make(chan ports.OutputFormats, 1)}
218- formats := make(chan ports.OutputFormats, 1)359+ s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: h.commands, OutputFormats: h.formats}, logging.For(context.Background(), "wayland"))
219- s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: commands, OutputFormats: formats}, logging.For(context.Background(), "wayland"))
220 if err != nil {360 if err != nil {
221 t.Fatal(err)361 t.Fatal(err)
222 }362 }
363+ h.s = s
223 ctx, cancel := context.WithCancel(context.Background())364 ctx, cancel := context.WithCancel(context.Background())
224 done := make(chan error, 1)365 done := make(chan error, 1)
225 go func() { done <- s.Run(ctx) }()366 go func() { done <- s.Run(ctx) }()
226 t.Cleanup(func() { cancel(); <-done })367 t.Cleanup(func() { cancel(); <-done })
227- c := protocolClient(t, s, dir)368+ h.c = protocolClient(t, s, dir)
228- w, surf, xdg := surfaceMapper(t, c, events)()369+ w, surf, xdg := surfaceMapper(t, h.c, events)()
370+ h.w = w
229 // Our configures must not block the mapper's one-slot proxy.371 // Our configures must not block the mapper's one-slot proxy.
230- registerProtocol(t, c, xdg)372+ registerProtocol(t, h.c, xdg)
231- dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)373+ dm := bindVersion(t, h.c, "zwp_linux_dmabuf_v1", version)
232- fb := c.AllocateID()374+ h.fb = h.c.AllocateID()
233- proxy := &feedbackProxy{rounds: make(chan []tranche, 8)}375+ h.proxy = &feedbackProxy{rounds: make(chan round, 8)}
234- proxy.SetID(fb)376+ h.proxy.SetID(h.fb)
235- registerWireProxy(c, proxy)377+ registerWireProxy(h.c, h.proxy)
236- requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, fb, surf)378+ requestProtocol(t, h.c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, h.fb, surf)
237- round := func(what string) []tranche {379+ return h
238- t.Helper()380+}
239- deadline := time.After(2 * time.Second)381+
240- for {382+// next waits for the next feedback round.
241- if err := c.Roundtrip(); err != nil {383+func (h *scanoutHarness) next(what string) round {
242- t.Fatal(err)384+ h.t.Helper()
243- }385+ deadline := time.After(2 * time.Second)
244- select {386+ for {
245- case r := <-proxy.rounds:387+ if err := h.c.Roundtrip(); err != nil {
246- return r388+ h.t.Fatal(err)
247- case <-deadline:389+ }
248- t.Fatalf("no feedback: %s", what)390+ select {
249- default:391+ case r := <-h.proxy.rounds:
250- time.Sleep(5 * time.Millisecond)392+ return r
251- }393+ case <-deadline:
394+ h.t.Fatalf("no feedback: %s", what)
395+ default:
396+ time.Sleep(5 * time.Millisecond)
252 }397 }
253 }398 }
399+}
400+
401+// A fullscreen surface on an output with scanout formats gets a scanout
402+// tranche first; leaving fullscreen sends the renderer tranche alone. From
403+// v6 the table comes once, main_device never, and the renderer tranche is
404+// flagged for sampling.
405+func TestDMABufScanoutTranche(t *testing.T) {
406+ tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
407+ sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
408+ h := newScanoutHarness(t, sup, dmabufVersion)
409+ c, s, w, fb, commands, formats := h.c, h.s, h.w, h.fb, h.commands, h.formats
254 scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)410 scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)
255- if r := round("initial"); len(r) != 1 || r[0].flags != 0 || len(r[0].indices) != 2 {411+ sampling := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsSampling)
412+ if r := h.next("initial"); len(r.tranches) != 1 || r.tranches[0].flags != sampling || len(r.tranches[0].indices) != 2 || r.tables != 1 || r.mainDevices != 0 {
256 t.Fatalf("tiled window: %+v", r)413 t.Fatalf("tiled window: %+v", r)
257 }414 }
258 formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}415 formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
259 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}416 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
260- r := round("fullscreen")417+ r := h.next("fullscreen")
261- for len(r) == 1 { // the formats may land after the configure418+ for len(r.tranches) == 1 { // the formats may land after the configure
262- r = round("fullscreen with formats")419+ if r.tables != 0 || r.mainDevices != 0 {
420+ t.Fatalf("resent table or main device: %+v", r)
421+ }
422+ r = h.next("fullscreen with formats")
263 }423 }
264- if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 {424+ if len(r.tranches) != 2 || r.tranches[0].flags != scanout || len(r.tranches[0].indices) != 1 || r.tranches[0].indices[0] != 1 || r.tranches[1].flags != sampling || r.tables != 0 || r.mainDevices != 0 {
265 t.Fatalf("fullscreen: %+v", r)425 t.Fatalf("fullscreen: %+v", r)
266 }426 }
267 // Hidden, the window loses its scanout tranche; shown, it gets it back.427 // Hidden, the window loses its scanout tranche; shown, it gets it back.
268 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}428 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
269- if r := round("hidden"); len(r) != 1 {429+ if r := h.next("hidden"); len(r.tranches) != 1 || r.tables != 0 {
270 t.Fatalf("hidden fullscreen: %+v", r)430 t.Fatalf("hidden fullscreen: %+v", r)
271 }431 }
272 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}432 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
273- if r := round("shown again"); len(r) != 2 || r[0].flags != scanout {433+ if r := h.next("shown again"); len(r.tranches) != 2 || r.tranches[0].flags != scanout || r.tables != 0 {
274 t.Fatalf("shown fullscreen: %+v", r)434 t.Fatalf("shown fullscreen: %+v", r)
275 }435 }
276 // Fullscreen on another output: its formats (none) apply at once.436 // Fullscreen on another output: its formats (none) apply at once.
277 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1", Visible: true}437 commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1", Visible: true}
278- if r := round("other output"); len(r) != 1 {438+ if r := h.next("other output"); len(r.tranches) != 1 || r.tables != 0 {
279 t.Fatalf("moved output: %+v", r)439 t.Fatalf("moved output: %+v", r)
280 }440 }
281 commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1", Visible: true}441 commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1", Visible: true}
282- if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 {442+ if r := h.next("tiled again"); len(r.tranches) != 1 || r.tranches[0].flags != sampling || r.tables != 0 {
283 t.Fatalf("left fullscreen: %+v", r)443 t.Fatalf("left fullscreen: %+v", r)
284 }444 }
285 // A destroyed feedback gets nothing more (a protocol error would445 // A destroyed feedback gets nothing more (a protocol error would
@@ -297,11 +457,79 @@ func TestDMABufScanoutTranche(t *testing.T) {
297 }457 }
298 }458 }
299 459
460+// A v4 client keeps the whole feedback on every round: table, main device,
461+// and an unflagged renderer tranche.
462+func TestDMABufFeedbackV4(t *testing.T) {
463+ tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
464+ sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
465+ h := newScanoutHarness(t, sup, 4)
466+ check := func(what string, r round, tranches int) {
467+ t.Helper()
468+ if r.tables != 1 || r.mainDevices != 1 || len(r.tranches) != tranches || r.tranches[len(r.tranches)-1].flags != 0 {
469+ t.Fatalf("%s: %+v", what, r)
470+ }
471+ }
472+ check("initial", h.next("initial"), 1)
473+ h.formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
474+ h.commands <- ports.ConfigureWindow{ID: h.w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
475+ r := h.next("fullscreen")
476+ for len(r.tranches) == 1 { // the formats may land after the configure
477+ check("fullscreen before formats", r, 1)
478+ r = h.next("fullscreen with formats")
479+ }
480+ check("fullscreen", r, 2)
481+ if r.tranches[0].flags != uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout) {
482+ t.Fatalf("scanout tranche: %+v", r)
483+ }
484+}
485+
486+// testDMABufGlobal is a global with one scanout offer for f's output.
487+func testDMABufGlobal(s *Server, f ports.OutputFormats) *dmabufGlobal {
488+ g := &dmabufGlobal{
489+ server: s,
490+ support: ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}},
491+ index: map[ports.DMABufFormat]uint16{linearARGB: 0},
492+ scanout: map[string]scanoutOffer{},
493+ }
494+ g.scanout[f.Output] = g.offer(f)
495+ return g
496+}
497+
498+func TestFullscreenOutput(t *testing.T) {
499+ windowSurface := func(mod func(*window)) *surface {
500+ w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
501+ mod(w)
502+ return &surface{xdg: &xdgSurface{window: w}}
503+ }
504+ tests := []struct {
505+ name string
506+ surf *surface
507+ out string
508+ ok bool
509+ }{
510+ {"nil", nil, "", false},
511+ {"destroyed", &surface{destroyed: true, xdg: &xdgSurface{window: &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Visible: true}}}}, "", false},
512+ {"no window", &surface{xdg: &xdgSurface{}}, "", false},
513+ {"not configured", windowSurface(func(w *window) { w.hasLast = false }), "", false},
514+ {"not fullscreen", windowSurface(func(w *window) { w.last.Fullscreen = false }), "", false},
515+ {"invisible", windowSurface(func(w *window) { w.last.Visible = false }), "", false},
516+ {"visible fullscreen", windowSurface(func(*window) {}), "DP-2", true},
517+ }
518+ g := testDMABufGlobal(&Server{}, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
519+ for _, tc := range tests {
520+ t.Run(tc.name, func(t *testing.T) {
521+ if out, ok := g.fullscreenOutput(tc.surf); out != tc.out || ok != tc.ok {
522+ t.Fatalf("got (%q, %v), want (%q, %v)", out, ok, tc.out, tc.ok)
523+ }
524+ })
525+ }
526+}
527+
300 func TestInvisibleScanoutOffer(t *testing.T) {528 func TestInvisibleScanoutOffer(t *testing.T) {
301 w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}529 w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
302 surf := &surface{xdg: &xdgSurface{window: w}}530 surf := &surface{xdg: &xdgSurface{window: w}}
303 s := &Server{}531 s := &Server{}
304- g := &dmabufGlobal{server: s, scanout: map[string]ports.OutputFormats{"DP-2": {Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}}}}532+ g := testDMABufGlobal(s, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
305 if _, ok := g.scanoutFor(surf); !ok {533 if _, ok := g.scanoutFor(surf); !ok {
306 t.Fatal("no visible scanout")534 t.Fatal("no visible scanout")
307 }535 }
@@ -330,7 +558,7 @@ func TestPopupFeedbackFollowsToplevel(t *testing.T) {
330 if toplevelRoot(pop) != top {558 if toplevelRoot(pop) != top {
331 t.Fatal("popup does not resolve to its toplevel")559 t.Fatal("popup does not resolve to its toplevel")
332 }560 }
333- g := &dmabufGlobal{server: &Server{}, scanout: map[string]ports.OutputFormats{"DP-2": {Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}}}}561+ g := testDMABufGlobal(&Server{}, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
334 if _, ok := g.scanoutFor(pop); !ok {562 if _, ok := g.scanoutFor(pop); !ok {
335 t.Fatal("popup of a visible fullscreen window has no offer")563 t.Fatal("popup of a visible fullscreen window has no offer")
336 }564 }

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