v0.3.0 2df0761

feedback_wire_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 4

↑↓ moveenter openesc close