1package neferclient_test
2
3import (
4 "encoding/binary"
5 "fmt"
6 "sync"
7 "testing"
8
9 "github.com/stretchr/testify/require"
10 "golang.org/x/sys/unix"
11
12 "github.com/bnema/neferclient"
13 neferclientmocks "github.com/bnema/neferclient/mocks"
14)
15
16const (
17 dmabufGlobal = 2 // names in surfaceGlobals
18 syncobjGlobal = 3
19)
20
21// feedbackPeer scripts zwp_linux_dmabuf_feedback_v1 rounds and tracks the
22// lifetime of every wl_buffer and timeline object the client creates, over the
23// wire peer. The peer answers a destructor with delete_id, so ids are recycled
24// as with a real compositor and lifetimes are tracked per live object. A
25// destructor is counted only for a live object: a second destroy of one would
26// leave created != destroyed or break the wire (the peer's delete_id for an id
27// already released is a protocol error, which fails the roundtrip).
28type feedbackPeer struct {
29 t *testing.T
30 srv *wireServer
31
32 mu sync.Mutex
33 feedback uint32 // the surface's feedback object
34 live map[uint32]bool // buffers and timelines created and not yet destroyed
35 params map[uint32]bool // zwp_linux_buffer_params_v1 objects
36 created int
37 destroyed int
38 violation []string // an id created while its previous object is still live
39}
40
41func newFeedbackPeer(t *testing.T, srv *wireServer) *feedbackPeer {
42 p := &feedbackPeer{t: t, srv: srv, live: map[uint32]bool{}, params: map[uint32]bool{}}
43 srv.mu.Lock()
44 srv.hook = func(obj uint32, op uint16, body []byte) {
45 srv.mu.Lock()
46 dmabuf, syncobj := srv.bound[dmabufGlobal].id, srv.bound[syncobjGlobal].id
47 srv.mu.Unlock()
48 p.mu.Lock()
49 defer p.mu.Unlock()
50 create := func(id uint32) {
51 if p.live[id] {
52 p.violation = append(p.violation, fmt.Sprintf("object %d created twice", id))
53 }
54 p.live[id] = true
55 p.created++
56 }
57 switch {
58 case obj == dmabuf && op == 1 && len(body) == 4: // create_params(new_id)
59 p.params[binary.LittleEndian.Uint32(body)] = true
60 case obj == dmabuf && op == 2 && len(body) == 4: // get_default_feedback(new_id)
61 p.feedback = binary.LittleEndian.Uint32(body)
62 case obj == syncobj && op == 2 && len(body) == 4: // import_timeline(new_id, fd)
63 create(binary.LittleEndian.Uint32(body))
64 case p.params[obj] && op == 3 && len(body) == 20: // params.create_immed(new_id, w, h, format, flags)
65 create(binary.LittleEndian.Uint32(body))
66 case p.params[obj] && op == 0 && len(body) == 0: // params.destroy: the id may be reused
67 delete(p.params, obj)
68 case op == 0 && len(body) == 0 && p.live[obj]: // destructor of a live buffer or timeline
69 delete(p.live, obj)
70 p.destroyed++
71 }
72 }
73 srv.mu.Unlock()
74 return p
75}
76
77// stats returns how many tracked objects were created, destroyed and are live.
78func (p *feedbackPeer) stats() (created, destroyed, live int, violations []string) {
79 p.mu.Lock()
80 defer p.mu.Unlock()
81 return p.created, p.destroyed, len(p.live), append([]string(nil), p.violation...)
82}
83
84// round sends one complete feedback round: the format table, the main device
85// and one tranche listing the given table indices.
86func (p *feedbackPeer) round(table []neferclient.Format, device uint64, indices ...uint16) {
87 p.t.Helper()
88 p.mu.Lock()
89 fb := p.feedback
90 p.mu.Unlock()
91 require.NotZero(p.t, fb, "the client requested its feedback object")
92
93 var entries []byte
94 for _, f := range table {
95 entries = binary.NativeEndian.AppendUint32(entries, f.FourCC)
96 entries = binary.NativeEndian.AppendUint32(entries, 0)
97 entries = binary.NativeEndian.AppendUint64(entries, f.Modifier)
98 }
99 fd, err := unix.MemfdCreate("table", unix.MFD_CLOEXEC)
100 require.NoError(p.t, err)
101 defer unix.Close(fd)
102 _, err = unix.Write(fd, entries)
103 require.NoError(p.t, err)
104 _, _, err = p.srv.conn.WriteMsgUnix(frame(fb, 1, appendU32(nil, uint32(len(entries)))), unix.UnixRights(fd), nil)
105 require.NoError(p.t, err)
106
107 dev := appendU32(nil, 8)
108 dev = binary.NativeEndian.AppendUint64(dev, device)
109 p.srv.write(frame(fb, 2, dev))
110 var idx []byte
111 for _, i := range indices {
112 idx = binary.NativeEndian.AppendUint16(idx, i)
113 }
114 n := len(idx)
115 for len(idx)%4 != 0 {
116 idx = append(idx, 0)
117 }
118 p.srv.write(frame(fb, 5, append(appendU32(nil, uint32(n)), idx...)))
119 p.srv.write(frame(fb, 3, nil)) // tranche_done
120 p.srv.write(frame(fb, 0, nil)) // done
121}
122
123func connectFeedbackSurface(t *testing.T) (*neferclient.Conn, *neferclient.Surface, *feedbackPeer) {
124 t.Helper()
125 c, srv := connectWire(t, surfaceGlobals, surfaceOutputs)
126 peer := newFeedbackPeer(t, srv)
127 s, err := c.NewToplevel("feedback", 64, 48)
128 require.NoError(t, err)
129 require.NoError(t, c.Roundtrip())
130 return c, s, peer
131}
132
133// TestFeedbackChangeAfterStartup: every complete round is reported; Equal tells
134// a changed main device or format table from a repeated round, and Clone keeps
135// the feedback a renderer was built from valid across later rounds.
136func TestFeedbackChangeAfterStartup(t *testing.T) {
137 c, s, peer := connectFeedbackSurface(t)
138 require.Nil(t, s.Feedback())
139
140 var done int
141 h := neferclientmocks.NewMockHandler(t)
142 h.EXPECT().FeedbackDone(s.ID()).Run(func(neferclient.SurfaceID) { done++ }).Return()
143 wait := func(n int) {
144 t.Helper()
145 dispatchUntil(t, c, h, func() bool { return done >= n })
146 }
147 table := []neferclient.Format{{FourCC: 0x34325258}, {FourCC: 0x34325241, Modifier: 7}, {FourCC: 0x34325258, Modifier: 1 << 40}}
148
149 peer.round(table, 0xe280, 0, 1)
150 wait(1)
151 inUse := s.Feedback().Clone() // what the first renderer was built from
152 require.Equal(t, &neferclient.Feedback{MainDevice: 0xe280, Formats: table[:2]}, inUse)
153 require.True(t, s.Feedback().Equal(inUse))
154
155 peer.round(table, 0xe280, 0, 1) // the compositor repeats its preference
156 wait(2)
157 require.True(t, s.Feedback().Equal(inUse), "a repeated round is not a change")
158
159 peer.round(table, 0xe281, 0, 1) // another main device
160 wait(3)
161 require.False(t, s.Feedback().Equal(inUse), "main device changed")
162 require.Equal(t, uint64(0xe280), inUse.MainDevice, "the clone is unaffected by the round")
163
164 peer.round(table, 0xe281, 0, 1, 2) // same device, more formats
165 wait(4)
166 changed := s.Feedback().Clone()
167 require.Equal(t, table, changed.Formats)
168
169 peer.round(table, 0xe281, 2, 1, 0) // same formats, other order: tranche order matters
170 wait(5)
171 require.False(t, s.Feedback().Equal(changed), "format order changed")
172 require.Len(t, changed.Formats, 3, "the clone keeps its own formats")
173 require.Equal(t, table[0], changed.Formats[0])
174 // The library double-buffers its format storage: this round writes the
175 // buffer the earlier round used, which a shallow clone would share.
176 peer.round(table, 0xe281, 2, 2, 2)
177 wait(6)
178 require.Equal(t, table, changed.Formats, "the clone does not alias library storage")
179 require.Equal(t, table[:2], inUse.Formats)
180
181 var none *neferclient.Feedback
182 require.True(t, none.Equal(nil))
183 require.False(t, none.Equal(inUse))
184 require.False(t, inUse.Equal(nil))
185 require.Nil(t, none.Clone())
186}
187
188// TestDestroyImportsWithPendingFrame: the buffers and timelines are dropped
189// while a frame is pending, the old ids are refused, no request goes to a
190// destroyed object, the new imports present once the frame fired, and every
191// object is destroyed exactly once, Close included.
192func TestDestroyImportsWithPendingFrame(t *testing.T) {
193 c, s, peer := connectFeedbackSurface(t)
194 s.MarkPresentable(64, 48)
195 surfaceID := s.SurfaceObjectID()
196 var callback, commits int
197 var cbMu sync.Mutex
198 peer.srv.mu.Lock()
199 inner := peer.srv.hook
200 peer.srv.hook = func(obj uint32, op uint16, body []byte) {
201 inner(obj, op, body)
202 if obj == surfaceID {
203 cbMu.Lock()
204 defer cbMu.Unlock()
205 switch {
206 case op == 3 && len(body) == 4:
207 callback = int(binary.LittleEndian.Uint32(body))
208 case op == 6:
209 commits++
210 }
211 }
212 }
213 peer.srv.mu.Unlock()
214
215 memfd, err := unix.MemfdCreate("imports", unix.MFD_CLOEXEC)
216 require.NoError(t, err)
217 defer unix.Close(memfd)
218 buf := &neferclient.Buffer{Width: 64, Height: 48, FourCC: 0x34325258, PlaneCount: 1,
219 Planes: [4]neferclient.Plane{{FD: memfd, Stride: 256}}}
220 imports := func() {
221 t.Helper()
222 require.NoError(t, s.ImportBuffer(1, buf))
223 require.NoError(t, s.ImportBuffer(2, buf))
224 require.NoError(t, s.ImportTimeline(1, memfd))
225 require.NoError(t, s.ImportTimeline(2, memfd))
226 }
227 imports()
228 p := &neferclient.Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 1, ReleasePoint: 2}
229 require.NoError(t, s.Present(p))
230 require.NoError(t, c.Roundtrip())
231 cbMu.Lock()
232 cb, committed := callback, commits
233 cbMu.Unlock()
234 require.NotZero(t, cb, "a frame is pending")
235 require.Positive(t, committed)
236 created, destroyed, live, bad := peer.stats()
237 require.Equal(t, [3]int{4, 0, 4}, [3]int{created, destroyed, live})
238 require.Empty(t, bad)
239
240 open := openFDs(t)
241 require.NoError(t, s.DestroyImports())
242 require.NoError(t, s.DestroyImports(), "nothing left to destroy")
243 require.NoError(t, c.Roundtrip())
244 require.Equal(t, open, openFDs(t), "no descriptor is closed or leaked")
245 created, destroyed, live, bad = peer.stats()
246 require.Equal(t, [3]int{4, 4, 0}, [3]int{created, destroyed, live}, "every old object destroyed exactly once")
247 require.Empty(t, bad)
248
249 // The old ids are gone: Present, DestroyBuffer and DestroyTimeline send nothing.
250 require.ErrorContains(t, s.Present(p), "frame callback pending") // gate first
251 require.ErrorContains(t, s.DestroyBuffer(1), "unknown buffer 1")
252 require.ErrorContains(t, s.DestroyTimeline(1), "unknown timeline 1")
253
254 // The frame fires; the surface is still presentable with re-imported ids.
255 h := neferclientmocks.NewMockHandler(t)
256 var framed bool
257 h.EXPECT().Frame(s.ID()).Run(func(neferclient.SurfaceID) { framed = true }).Return()
258 peer.srv.write(frame(uint32(cb), 0, appendU32(nil, 1)))
259 peer.srv.write(frame(1, 1, appendU32(nil, uint32(cb))))
260 dispatchUntil(t, c, h, func() bool { return framed })
261 require.ErrorContains(t, s.Present(p), "unknown buffer 1")
262 imports()
263 require.NoError(t, s.Present(p))
264 require.NoError(t, c.Roundtrip())
265 created, destroyed, live, bad = peer.stats()
266 require.Equal(t, [3]int{8, 4, 4}, [3]int{created, destroyed, live})
267 require.Empty(t, bad)
268
269 // Close destroys what is imported now, and only that.
270 require.NoError(t, s.Close())
271 require.ErrorIs(t, s.DestroyImports(), neferclient.ErrClosed)
272 require.NoError(t, c.Roundtrip())
273 created, destroyed, live, bad = peer.stats()
274 require.Equal(t, [3]int{8, 8, 0}, [3]int{created, destroyed, live}, "Close destroys the new objects once")
275 require.Empty(t, bad)
276 require.NoError(t, c.Dispatch(nil))
277}