v0.3.0 cf370a0

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/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, 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", Visible: true}
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		registerWireProxy(c, 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 := wireRequest(c, 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	// With no flip, hiding must discard a waiting feedback immediately.
162	hidden := feedback()
163	commit()
164	commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: false}
165	if ev := answer(hidden, "hidden without flip"); !ev.discarded {
166		t.Fatalf("hidden feedback: %+v", ev)
167	}
168	commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: true}
169	// Wait for the resume before committing the next feedback: the command
170	// goroutine applies it in its own display round trip, which an empty Do
171	// here may overtake.
172	for deadline := time.Now().Add(2 * time.Second); ; {
173		var visible bool
174		s.display.Do(func() { visible = s.windows[w.ID].last.Visible })
175		if visible {
176			break
177		}
178		if time.Now().After(deadline) {
179			t.Fatal("resume not applied")
180		}
181		time.Sleep(2 * time.Millisecond)
182	}
183	// Presented with the flip's time, counter and flags.
184	a := feedback()
185	commit()
186	seqA := lastSeq()
187	flip(5*time.Second+7, 42, seqA, w.ID)
188	ev := answer(a, "presented")
189	if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 {
190		t.Fatalf("presented %+v", ev)
191	}
192	want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy)
193	if ev.flags != want {
194		t.Fatalf("flags %#x want %#x", ev.flags, want)
195	}
196	// Two flips in one drain: two presented events, each with its time.
197	b := feedback()
198	commit()
199	seqB := lastSeq()
200	d := feedback()
201	commit()
202	seqD := lastSeq()
203	flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay
204	flip(7*time.Second, 44, seqD, 0)
205	if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 {
206		t.Fatalf("first %+v", ev)
207	}
208	if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 {
209		t.Fatalf("second %+v", ev)
210	}
211	// Replaced before shown: discarded when the flip shows the newer one.
212	old := feedback()
213	commit()
214	lastSeq()
215	commit()
216	seqNew := lastSeq()
217	flip(8*time.Second, 45, seqNew, 0)
218	if ev := answer(old, "replaced"); !ev.discarded {
219		t.Fatalf("replaced: %+v", ev)
220	}
221	// A subsurface's feedback follows its root: presented with the root's
222	// content that the flip shows.
223	comp := bindProtocol(t, c, "wl_compositor")
224	sub := bindProtocol(t, c, "wl_subcompositor")
225	child, subsurf := c.AllocateID(), c.AllocateID()
226	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
227	registerProtocol(t, c, child)
228	requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf)
229	registerProtocol(t, c, subsurf)
230	cfb := c.AllocateID()
231	cp := &feedbackEvents{out: make(chan presentedEvent, 1)}
232	cp.SetID(cfb)
233	registerWireProxy(c, cp)
234	requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb)
235	requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
236	commit() // the parent applies the subsurface
237	seqC := lastSeq()
238	flip(8500*time.Millisecond, 50, seqC, 0)
239	if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 {
240		t.Fatalf("subsurface %+v", ev)
241	}
242	// Surface destroyed: discarded.
243	gone := feedback()
244	commit()
245	lastSeq()
246	requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
247	flip(9*time.Second, 46, 0, 0)
248	if ev := answer(gone, "destroyed"); !ev.discarded {
249		t.Fatalf("destroyed: %+v", ev)
250	}
251}
252
253// A hidden window (no output) gets discarded at once.
254func TestPresentationHiddenDiscarded(t *testing.T) {
255	s, events, _, _, _, dir := presentServer(t)
256	c := protocolClient(t, s, dir)
257	_, surf, _ := surfaceMapper(t, c, events)()
258	pres := bindProtocol(t, c, "wp_presentation")
259	registerProtocol(t, c, pres)
260	fb := c.AllocateID()
261	p := &feedbackEvents{out: make(chan presentedEvent, 1)}
262	p.SetID(fb)
263	registerWireProxy(c, p)
264	requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
265	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
266	if err := c.Roundtrip(); err != nil {
267		t.Fatal(err)
268	}
269	if err := c.Roundtrip(); err != nil {
270		t.Fatal(err)
271	}
272	select {
273	case ev := <-p.out:
274		if !ev.discarded {
275			t.Fatalf("%+v", ev)
276		}
277	default:
278		t.Fatal("not discarded")
279	}
280}
281
282// Commit timing: a commit targeted two refreshes ahead of the last flip
283// is not applied on the next one.
284func TestNextRefreshFollowsFlips(t *testing.T) {
285	s := &Server{flips: map[string]ports.FlipInfo{}}
286	s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}}
287	now := time.Now()
288	mono := monotonic(now)
289	period := time.Second / 60
290	s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period}
291	next := s.nextRefresh("A", now)
292	if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond {
293		t.Fatalf("next refresh in %v, want %v", d, period*3/4)
294	}
295}
296
297// Software flips (headless) answer feedback without flags and do not pace
298// frame callbacks.
299func TestSoftwareFlipsDoNotPace(t *testing.T) {
300	if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) {
301		t.Fatal("software flip paces callbacks")
302	}
303	if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) {
304		t.Fatal("display flip does not pace")
305	}
306}

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 25

Tags 10

↑↓ moveenter openesc close