v0.1.0 d9218d6

presentation_test.go RawHistoryFolder

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

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