v0.10.0 d5d679d

presentation_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"encoding/binary"
6	"testing"
7	"time"
8
9	"github.com/bnema/go-wayland-bindings/server/presentationtime"
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/neferwl/internal/adapters/logging"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/wlturbo"
14	"golang.org/x/sys/unix"
15)
16
17// presentedEvent is one feedback answer: presented (with its fields) or
18// discarded.
19type presentedEvent struct {
20	discarded   bool
21	when        time.Duration
22	refresh     uint32
23	seq         uint64
24	flags       uint32
25	syncOutputs int
26}
27
28type feedbackEvents struct {
29	wlturbo.BaseProxy
30	out  chan presentedEvent
31	sync int
32}
33
34func (p *feedbackEvents) Dispatch(e *wlturbo.Event) {
35	switch uint32(e.Opcode) {
36	case presentationtime.WpPresentationFeedbackEventSyncOutput:
37		p.sync++
38	case presentationtime.WpPresentationFeedbackEventPresented:
39		d := e.Data()
40		u := func(i int) uint32 { return binary.NativeEndian.Uint32(d[i*4:]) }
41		sec := uint64(u(0))<<32 | uint64(u(1))
42		p.out <- presentedEvent{when: time.Duration(sec)*time.Second + time.Duration(u(2)), refresh: u(3), seq: uint64(u(4))<<32 | uint64(u(5)), flags: u(6), syncOutputs: p.sync}
43	case presentationtime.WpPresentationFeedbackEventDiscarded:
44		p.out <- presentedEvent{discarded: true}
45	}
46}
47
48// presentServer is a server whose flips the test sends.
49func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, <-chan ports.SurfaceContent, ports.PresentationSink, string) {
50	t.Helper()
51	return presentServerOn(t, testOutputs)
52}
53
54// presentServerOn is presentServer with its own output layout.
55func presentServerOn(t *testing.T, layout ports.Layout) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, <-chan ports.SurfaceContent, ports.PresentationSink, string) {
56	t.Helper()
57	dir := t.TempDir()
58	events := make(chan ports.ClientEvent, 16)
59	commands := make(chan ports.ClientCommand, 16)
60	s, err := New(Options{RuntimeDir: dir, Outputs: layout}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland"))
61	if err != nil {
62		t.Fatal(err)
63	}
64	contents := relayContents(t, s)
65	ctx, cancel := context.WithCancel(context.Background())
66	done := make(chan error, 1)
67	go func() { done <- s.Run(ctx) }()
68	t.Cleanup(func() { cancel(); <-done })
69	return s, events, commands, contents, s.Presentation(), dir
70}
71
72func TestPresentationFeedback(t *testing.T) {
73	s, events, commands, contents, presented, dir := presentServer(t)
74	c := protocolClient(t, s, dir)
75	w, surf, xdg := surfaceMapper(t, c, events)()
76	registerProtocol(t, c, xdg)
77	commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: true}
78	for deadline := time.Now().Add(2 * time.Second); ; {
79		var out string
80		s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
81		if out != "" {
82			break
83		}
84		if time.Now().After(deadline) {
85			t.Fatal("configure not applied")
86		}
87		time.Sleep(2 * time.Millisecond)
88	}
89	registerProtocol(t, c, bindProtocol(t, c, "wl_output"))
90	pres := bindProtocol(t, c, "wp_presentation")
91	registerProtocol(t, c, pres)
92	// Drain contents so seqs are known.
93	lastSeq := func() uint64 {
94		t.Helper()
95		var seq uint64
96		deadline := time.After(2 * time.Second)
97		for {
98			select {
99			case got := <-contents:
100				if got.ID == w.ID {
101					seq = got.Seq
102				}
103			case <-time.After(50 * time.Millisecond):
104				if seq != 0 {
105					return seq
106				}
107			case <-deadline:
108				t.Fatal("no content")
109			}
110		}
111	}
112	feedback := func() *feedbackEvents {
113		t.Helper()
114		fb := c.AllocateID()
115		p := &feedbackEvents{out: make(chan presentedEvent, 4)}
116		p.SetID(fb)
117		registerWireProxy(c, p)
118		requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
119		return p
120	}
121	answer := func(p *feedbackEvents, what string) presentedEvent {
122		t.Helper()
123		deadline := time.Now().Add(2 * time.Second)
124		for time.Now().Before(deadline) {
125			if err := c.Roundtrip(); err != nil {
126				t.Fatal(err)
127			}
128			select {
129			case ev := <-p.out:
130				return ev
131			case <-time.After(5 * time.Millisecond):
132			}
133		}
134		t.Fatalf("no answer: %s", what)
135		return presentedEvent{}
136	}
137	// A fresh 1×1 buffer per commit: every commit is a new content.
138	shm := bindProtocol(t, c, "wl_shm")
139	registerProtocol(t, c, shm)
140	fd, err := unix.MemfdCreate("present", 0)
141	if err != nil {
142		t.Fatal(err)
143	}
144	defer unix.Close(fd)
145	if err := unix.Ftruncate(fd, 4); err != nil {
146		t.Fatal(err)
147	}
148	pool := c.AllocateID()
149	registerProtocol(t, c, pool)
150	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
151		t.Fatal(err)
152	}
153	commit := func() {
154		buf := c.AllocateID()
155		registerProtocol(t, c, buf)
156		requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
157		requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
158		requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
159		if err := c.Roundtrip(); err != nil {
160			t.Fatal(err)
161		}
162	}
163	flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) {
164		presented.Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}})
165	}
166	// With no flip, hiding must discard a waiting feedback immediately.
167	hidden := feedback()
168	commit()
169	commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: false}
170	if ev := answer(hidden, "hidden without flip"); !ev.discarded {
171		t.Fatalf("hidden feedback: %+v", ev)
172	}
173	commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: true}
174	// Wait for the resume before committing the next feedback: the command
175	// goroutine applies it in its own display round trip, which an empty Do
176	// here may overtake.
177	for deadline := time.Now().Add(2 * time.Second); ; {
178		var visible bool
179		s.display.Do(func() { visible = s.windows[w.ID].last.Visible })
180		if visible {
181			break
182		}
183		if time.Now().After(deadline) {
184			t.Fatal("resume not applied")
185		}
186		time.Sleep(2 * time.Millisecond)
187	}
188	// Presented with the flip's time, counter and flags.
189	a := feedback()
190	commit()
191	seqA := lastSeq()
192	flip(5*time.Second+7, 42, seqA, w.ID)
193	ev := answer(a, "presented")
194	if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 {
195		t.Fatalf("presented %+v", ev)
196	}
197	want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy)
198	if ev.flags != want {
199		t.Fatalf("flags %#x want %#x", ev.flags, want)
200	}
201	// Two flips in one drain: two presented events, each with its time.
202	b := feedback()
203	commit()
204	seqB := lastSeq()
205	d := feedback()
206	commit()
207	seqD := lastSeq()
208	flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay
209	flip(7*time.Second, 44, seqD, 0)
210	if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 {
211		t.Fatalf("first %+v", ev)
212	}
213	if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 {
214		t.Fatalf("second %+v", ev)
215	}
216	// Replaced before shown: discarded when the flip shows the newer one.
217	old := feedback()
218	commit()
219	lastSeq()
220	commit()
221	seqNew := lastSeq()
222	flip(8*time.Second, 45, seqNew, 0)
223	if ev := answer(old, "replaced"); !ev.discarded {
224		t.Fatalf("replaced: %+v", ev)
225	}
226	// A subsurface's feedback follows its root: presented with the root's
227	// content that the flip shows.
228	comp := bindProtocol(t, c, "wl_compositor")
229	sub := bindProtocol(t, c, "wl_subcompositor")
230	child, subsurf := c.AllocateID(), c.AllocateID()
231	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
232	registerProtocol(t, c, child)
233	requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf)
234	registerProtocol(t, c, subsurf)
235	cfb := c.AllocateID()
236	cp := &feedbackEvents{out: make(chan presentedEvent, 1)}
237	cp.SetID(cfb)
238	registerWireProxy(c, cp)
239	requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb)
240	requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
241	commit() // the parent applies the subsurface
242	seqC := lastSeq()
243	flip(8500*time.Millisecond, 50, seqC, 0)
244	if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 {
245		t.Fatalf("subsurface %+v", ev)
246	}
247	// Surface destroyed: discarded.
248	gone := feedback()
249	commit()
250	lastSeq()
251	requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
252	flip(9*time.Second, 46, 0, 0)
253	if ev := answer(gone, "destroyed"); !ev.discarded {
254		t.Fatalf("destroyed: %+v", ev)
255	}
256}
257
258// A hidden window (no output) gets discarded at once.
259func TestPresentationHiddenDiscarded(t *testing.T) {
260	s, events, _, _, _, dir := presentServer(t)
261	c := protocolClient(t, s, dir)
262	_, surf, _ := surfaceMapper(t, c, events)()
263	pres := bindProtocol(t, c, "wp_presentation")
264	registerProtocol(t, c, pres)
265	fb := c.AllocateID()
266	p := &feedbackEvents{out: make(chan presentedEvent, 1)}
267	p.SetID(fb)
268	registerWireProxy(c, p)
269	requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
270	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
271	if err := c.Roundtrip(); err != nil {
272		t.Fatal(err)
273	}
274	if err := c.Roundtrip(); err != nil {
275		t.Fatal(err)
276	}
277	select {
278	case ev := <-p.out:
279		if !ev.discarded {
280			t.Fatalf("%+v", ev)
281		}
282	default:
283		t.Fatal("not discarded")
284	}
285}
286
287// Commit timing: a commit targeted two refreshes ahead of the last flip
288// is not applied on the next one.
289func TestNextRefreshFollowsFlips(t *testing.T) {
290	s := &Server{flips: map[string]ports.FlipInfo{}}
291	s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}}
292	now := time.Now()
293	mono := monotonic(now)
294	period := time.Second / 60
295	s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period}
296	next := s.nextRefresh("A", now)
297	if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond {
298		t.Fatalf("next refresh in %v, want %v", d, period*3/4)
299	}
300}
301
302// Software flips (headless) answer feedback without flags and do not pace
303// frame callbacks.
304func TestSoftwareFlipsDoNotPace(t *testing.T) {
305	if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) {
306		t.Fatal("software flip paces callbacks")
307	}
308	if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) {
309		t.Fatal("display flip does not pace")
310	}
311}

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 23

Tags 10

↑↓ moveenter openesc close