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 Unverified
B5690EEEBB952194The 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 | |||
| 53 | 53 | ||
| 54 | 54 | ## 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 | 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 | 58 | - `wp_tearing_control_v1` and VRR while a fullscreen window covers the output. | |
| 59 | 59 | - `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 path | 54 | ## 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 ( | |||
| 13 | 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 | 17 | // The formats are those the renderer imports (ports.DMABufSupport); without | |
| 18 | 18 | // any, the global is not advertised and clients use wl_shm. Surface | |
| 19 | 19 | // feedback also offers, first, a scanout tranche while the surface's | |
| 20 | 20 | // window is fullscreen on an output that reported scanout formats | |
| 21 | 21 | // (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 = 4 | |
| 29 | + | const dmabufVersion = 6 | |
| 25 | 30 | ||
| 26 | 31 | // maxPlanes is the protocol limit (DRM planes). | |
| 27 | 32 | const maxPlanes = 4 | |
| @@ -34,7 +39,24 @@ func registerDMABuf(d *server.Display, s *Server, sup ports.DMABufSupport) error | |||
| 34 | 39 | if err != nil { | |
| 35 | 40 | 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 | 60 | return linuxdmabuf.NewZwpLinuxDmabufV1Global(d, dmabufVersion, func(c server.Client, v, id uint32) { | |
| 39 | 61 | r, err := linuxdmabuf.NewZwpLinuxDmabufV1(c, int32(v), id, s.dmabuf) | |
| 40 | 62 | if err != nil || v >= 4 { | |
| @@ -81,11 +103,42 @@ type dmabufGlobal struct { | |||
| 81 | 103 | support ports.DMABufSupport | |
| 82 | 104 | table *os.File | |
| 83 | 105 | 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 | |
| 89 | 142 | } | |
| 90 | 143 | ||
| 91 | 144 | func (g *dmabufGlobal) close() { | |
| @@ -121,9 +174,10 @@ func (g *dmabufGlobal) feedback(r *linuxdmabuf.ZwpLinuxDmabufV1, id uint32, surf | |||
| 121 | 174 | if err != nil { | |
| 122 | 175 | return | |
| 123 | 176 | } | |
| 124 | - | g.feedbacks[fb] = surf | |
| 177 | + | st := &feedbackState{surf: surf} | |
| 178 | + | g.feedbacks[fb] = st | |
| 125 | 179 | fb.OnDestroy = func() { delete(g.feedbacks, fb) } | |
| 126 | - | g.send(fb, surf) | |
| 180 | + | g.send(fb, st) | |
| 127 | 181 | } | |
| 128 | 182 | ||
| 129 | 183 | // devBytes is a dev_t as the protocol's array. | |
| @@ -133,49 +187,57 @@ func devBytes(dev uint64) []byte { | |||
| 133 | 187 | return b | |
| 134 | 188 | } | |
| 135 | 189 | ||
| 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) | |
| 163 | 215 | fb.SendTrancheDone() | |
| 164 | 216 | fb.SendDone() | |
| 165 | 217 | } | |
| 166 | 218 | ||
| 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) { | |
| 170 | 222 | if surf == nil || surf.destroyed { | |
| 171 | - | return ports.OutputFormats{}, false | |
| 223 | + | return "", false | |
| 172 | 224 | } | |
| 173 | 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 { | |
| 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 | |
| 176 | 228 | } | |
| 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 | |
| 179 | 241 | } | |
| 180 | 242 | ||
| 181 | 243 | // setOutputFormats records an output's scanout formats and sends the | |
| @@ -183,19 +245,19 @@ func (g *dmabufGlobal) scanoutFor(surf *surface) (ports.OutputFormats, bool) { | |||
| 183 | 245 | // formats the renderer samples (a refused buffer is composed), so they | |
| 184 | 246 | // are all in the table. | |
| 185 | 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 | 249 | return | |
| 188 | 250 | } | |
| 189 | 251 | if len(f.Formats) == 0 { | |
| 190 | 252 | delete(g.scanout, f.Output) | |
| 191 | 253 | } 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) | |
| 193 | 257 | } | |
| 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) | |
| 199 | 261 | } | |
| 200 | 262 | } | |
| 201 | 263 | } | |
| @@ -206,10 +268,10 @@ func (g *dmabufGlobal) resendSurface(root *surface) { | |||
| 206 | 268 | if g == nil { | |
| 207 | 269 | return | |
| 208 | 270 | } | |
| 209 | - | for fb, surf := range g.feedbacks { | |
| 271 | + | for fb, st := range g.feedbacks { | |
| 210 | 272 | // 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) | |
| 213 | 275 | } | |
| 214 | 276 | } | |
| 215 | 277 | } | |
| @@ -299,10 +361,17 @@ func (p *params) build(width, height int32, format, flags uint32) (buf *dmabufBu | |||
| 299 | 361 | r.PostError(uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDimensions), "invalid dimensions") | |
| 300 | 362 | return nil, false, false | |
| 301 | 363 | } | |
| 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 | + | } | |
| 306 | 375 | return nil, true, true | |
| 307 | 376 | } | |
| 308 | 377 | // 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 | 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 | 473 | // dmabufBuffer is a wl_buffer backed by client GPU memory. | |
| 396 | 474 | 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); without | 17 | // The formats are those the renderer imports (ports.DMABufSupport); without | ||
| 18 | // any, the global is not advertised and clients use wl_shm. Surface | 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 | 19 | // feedback also offers, first, a scanout tranche while the surface's | ||
| 20 | // window is fullscreen on an output that reported scanout formats | 20 | // window is fullscreen on an output that reported scanout formats | ||
| 21 | // (ports.OutputFormats): a buffer allocated from it can be flipped to the | 21 | // (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 = 4 | 29 | + | const dmabufVersion = 6 |
| 25 | 30 | ||||
| 26 | // maxPlanes is the protocol limit (DRM planes). | 31 | // maxPlanes is the protocol limit (DRM planes). | ||
| 27 | const maxPlanes = 4 | 32 | 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 err | 40 | 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.DMABufSupport | 103 | support ports.DMABufSupport | ||
| 82 | table *os.File | 104 | table *os.File | ||
| 83 | nextID uint64 | 105 | 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.OutputFormats | 107 | + | // 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]*surface | 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 | |||
| 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 | return | 175 | return | ||
| 123 | } | 176 | } | ||
| 124 | - | g.feedbacks[fb] = surf | 177 | + | 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 b | 187 | return b | ||
| 134 | } | 188 | } | ||
| 135 | 189 | ||||
| 136 | - | // send sends the whole feedback: table, main device, the scanout tranche | 190 | + | // 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 []byte | 197 | + | 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 fullscreen | 219 | + | // 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{}, false | 223 | + | 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{}, false | 227 | + | 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) > 0 | 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 | |||
| 179 | } | 241 | } | ||
| 180 | 242 | ||||
| 181 | // setOutputFormats records an output's scanout formats and sends the | 243 | // 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 they | 245 | // 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 | return | 249 | 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] = f | 254 | + | // 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 | return | 269 | 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, false | 362 | return nil, false, false | ||
| 301 | } | 363 | } | ||
| 302 | - | // Unsupported formats, y-inverted or interlaced buffers: the client | 364 | + | // 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, true | 375 | return nil, true, true | ||
| 307 | } | 376 | } | ||
| 308 | // NV12/P010 are 4:2:0 two-plane images. The same modifier must | 377 | // 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 { | |||
| 57 | 57 | ||
| 58 | 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 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. | |
| 62 | 62 | func TestDMABufParams(t *testing.T) { | |
| 63 | 63 | s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}}) | |
| 64 | 64 | c := protocolClient(t, s, dir) | |
| @@ -93,13 +93,8 @@ func TestDMABufParams(t *testing.T) { | |||
| 93 | 93 | } | |
| 94 | 94 | 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 | 96 | // Supported: created (import errors surface in the renderer). | |
| 102 | - | _, result = create(linearARGB.Format, 16) | |
| 97 | + | _, result := create(linearARGB.Format, 16) | |
| 103 | 98 | if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) { | |
| 104 | 99 | t.Fatalf("event %d", op) | |
| 105 | 100 | } | |
| @@ -111,6 +106,132 @@ func TestDMABufParams(t *testing.T) { | |||
| 111 | 106 | } | |
| 112 | 107 | requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0)) | |
| 113 | 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 := ¶msProxy{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 := ¶msProxy{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 | 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 | 298 | type feedbackProxy struct { | |
| 178 | 299 | wlturbo.BaseProxy | |
| 179 | - | rounds chan []tranche | |
| 180 | - | cur []tranche | |
| 300 | + | rounds chan round | |
| 301 | + | cur round | |
| 181 | 302 | 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 | 312 | type tranche struct { | |
| 185 | 313 | flags uint32 | |
| 186 | 314 | indices []uint16 | |
| @@ -189,9 +317,12 @@ type tranche struct { | |||
| 189 | 317 | func (p *feedbackProxy) Dispatch(e *wlturbo.Event) { | |
| 190 | 318 | switch uint32(e.Opcode) { | |
| 191 | 319 | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable: | |
| 320 | + | p.cur.tables++ | |
| 192 | 321 | if fd := e.Fd(); fd != 0 { | |
| 193 | 322 | unix.Close(int(fd)) | |
| 194 | 323 | } | |
| 324 | + | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventMainDevice: | |
| 325 | + | p.cur.mainDevices++ | |
| 195 | 326 | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags: | |
| 196 | 327 | p.open.flags = e.Uint32() | |
| 197 | 328 | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats: | |
| @@ -200,86 +331,115 @@ func (p *feedbackProxy) Dispatch(e *wlturbo.Event) { | |||
| 200 | 331 | p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8) | |
| 201 | 332 | } | |
| 202 | 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 | 335 | case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone: | |
| 205 | 336 | p.rounds <- p.cur | |
| 206 | - | p.cur = nil | |
| 337 | + | p.cur = round{} | |
| 207 | 338 | } | |
| 208 | 339 | } | |
| 209 | 340 | ||
| 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() | |
| 215 | 356 | dir := t.TempDir() | |
| 216 | 357 | 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")) | |
| 220 | 360 | if err != nil { | |
| 221 | 361 | t.Fatal(err) | |
| 222 | 362 | } | |
| 363 | + | h.s = s | |
| 223 | 364 | ctx, cancel := context.WithCancel(context.Background()) | |
| 224 | 365 | done := make(chan error, 1) | |
| 225 | 366 | go func() { done <- s.Run(ctx) }() | |
| 226 | 367 | 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 | |
| 229 | 371 | // 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) | |
| 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 | 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 | 413 | t.Fatalf("tiled window: %+v", r) | |
| 257 | 414 | } | |
| 258 | 415 | formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}} | |
| 259 | 416 | 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") | |
| 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 | 425 | t.Fatalf("fullscreen: %+v", r) | |
| 266 | 426 | } | |
| 267 | 427 | // Hidden, the window loses its scanout tranche; shown, it gets it back. | |
| 268 | 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 | 430 | t.Fatalf("hidden fullscreen: %+v", r) | |
| 271 | 431 | } | |
| 272 | 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 | 434 | t.Fatalf("shown fullscreen: %+v", r) | |
| 275 | 435 | } | |
| 276 | 436 | // Fullscreen on another output: its formats (none) apply at once. | |
| 277 | 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 | 439 | t.Fatalf("moved output: %+v", r) | |
| 280 | 440 | } | |
| 281 | 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 | 443 | t.Fatalf("left fullscreen: %+v", r) | |
| 284 | 444 | } | |
| 285 | 445 | // 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 | 528 | func TestInvisibleScanoutOffer(t *testing.T) { | |
| 301 | 529 | w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}} | |
| 302 | 530 | surf := &surface{xdg: &xdgSurface{window: w}} | |
| 303 | 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 | 533 | if _, ok := g.scanoutFor(surf); !ok { | |
| 306 | 534 | t.Fatal("no visible scanout") | |
| 307 | 535 | } | |
| @@ -330,7 +558,7 @@ func TestPopupFeedbackFollowsToplevel(t *testing.T) { | |||
| 330 | 558 | if toplevelRoot(pop) != top { | |
| 331 | 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 | 562 | if _, ok := g.scanoutFor(pop); !ok { | |
| 335 | 563 | t.Fatal("popup of a visible fullscreen window has no offer") | |
| 336 | 564 | } | |
| @@ -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 its | 60 | + | // 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.result | 94 | 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 := ¶msProxy{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 := ¶msProxy{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.BaseProxy | 299 | wlturbo.BaseProxy | ||
| 179 | - | rounds chan []tranche | 300 | + | rounds chan round |
| 180 | - | cur []tranche | 301 | + | cur round |
| 181 | open tranche | 302 | 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 uint32 | 313 | flags uint32 | ||
| 186 | indices []uint16 | 314 | 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.cur | 336 | p.rounds <- p.cur | ||
| 206 | - | p.cur = nil | 337 | + | p.cur = round{} |
| 207 | } | 338 | } | ||
| 208 | } | 339 | } | ||
| 209 | 340 | ||||
| 210 | - | // A fullscreen surface on an output with scanout formats gets a scanout | 341 | + | // 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 r | 388 | + | 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 configure | 418 | + | 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 would | 445 | // 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 | } | ||