frames_test.go RawHistoryFolder

1package wayland
2
3import (
4	"slices"
5	"testing"
6	"time"
7
8	"github.com/bnema/go-wayland-bindings/server/wayland"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12func TestDueFramesAllocations(t *testing.T) {
13	s := &Server{frameDue: map[string]time.Time{}, awaiting: map[string][]*wayland.Callback{}}
14	now := time.Unix(100, 0)
15	s.dueFrames(now, nil)
16	if allocs := testing.AllocsPerRun(100, func() { s.dueFrames(now, nil) }); allocs != 0 {
17		t.Errorf("idle dueFrames: %.1f allocs, want 0", allocs)
18	} else {
19		t.Logf("idle dueFrames: %.1f allocs", allocs)
20	}
21}
22
23func TestFramePeriod(t *testing.T) {
24	for _, tc := range []struct {
25		milli int
26		want  time.Duration
27	}{{0, time.Second / 60}, {60000, time.Second / 60}, {165058, 6058476}, {144000, 6944444}} {
28		if got := framePeriod(tc.milli); got != tc.want {
29			t.Errorf("%d: %v want %v", tc.milli, got, tc.want)
30		}
31	}
32}
33
34// Each output is paced at its own refresh; a flip fires its queue and
35// pushes its deadline back; the pacer idles with nothing queued.
36func TestDueFrames(t *testing.T) {
37	fast := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 165000}}}
38	slow := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "HDMI-A-1", RefreshMilli: 60000}}}
39	s := &Server{outputs: []*output{fast, slow}, frameDue: map[string]time.Time{}, awaiting: map[string][]*wayland.Callback{}}
40	// dueFrames only counts the queued callbacks: nil ones stand for them.
41	queue := func(name string) { s.awaiting[name] = append(s.awaiting[name], nil) }
42	fired := func(fire []string) {
43		for _, name := range fire {
44			delete(s.awaiting, name)
45		}
46	}
47	now := time.Unix(100, 0)
48	if _, _, idle := s.dueFrames(now, nil); !idle {
49		t.Fatal("not idle without callbacks")
50	}
51	// A first callback goes out at once, then one per refresh.
52	queue("DP-2")
53	queue("HDMI-A-1")
54	fire, _, idle := s.dueFrames(now, nil)
55	if len(fire) != 2 || !idle {
56		t.Fatalf("first fire %v idle %v", fire, idle)
57	}
58	fired(fire)
59	queue("DP-2")
60	queue("HDMI-A-1")
61	fire, wait, _ := s.dueFrames(now.Add(time.Millisecond), nil)
62	if len(fire) != 0 || wait != framePeriod(165000)-time.Millisecond {
63		t.Fatalf("early fire %v wait %v", fire, wait)
64	}
65	at := now.Add(framePeriod(165000))
66	fire, wait, _ = s.dueFrames(at, nil)
67	if !slices.Equal(fire, []string{"DP-2"}) || wait != time.Second/60-framePeriod(165000) {
68		t.Fatalf("165 Hz fire %v wait %v", fire, wait)
69	}
70	fired(fire)
71	// A flip fires its output early and pushes its deadline to 1.5 periods.
72	queue("DP-2")
73	flip := at.Add(time.Millisecond)
74	fire, _, _ = s.dueFrames(flip, map[string]bool{"DP-2": true})
75	if !slices.Equal(fire, []string{"DP-2"}) || s.frameDue["DP-2"] != flip.Add(framePeriod(165000)*3/2) {
76		t.Fatalf("flip fire %v due %v", fire, s.frameDue["DP-2"].Sub(flip))
77	}
78	fired(fire)
79	// A queue on an unplugged output moves to the outputless one (60 Hz).
80	s.outputs = []*output{fast}
81	queue("HDMI-A-1")
82	fire, _, _ = s.dueFrames(flip, nil)
83	fired(fire)
84	if _, ok := s.frameDue["HDMI-A-1"]; ok || len(s.awaiting["HDMI-A-1"]) != 0 || !slices.Equal(fire, []string{""}) {
85		t.Fatalf("unplugged: fire %v due %v", fire, s.frameDue)
86	}
87}
88
89func TestInvisibleFramePacingAndMigration(t *testing.T) {
90	out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
91	s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1)}
92	w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
93	root := &surface{xdg: &xdgSurface{window: w}}
94	child := &surface{sub: subState{parent: root}}
95	cursor := &surface{}
96	// Use distinct live callback pointers as identities; dueFrames never dereferences them.
97	a, b := new(wayland.Callback), new(wayland.Callback)
98	s.queueFrames(child, []*wayland.Callback{a})
99	s.queueFrames(cursor, []*wayland.Callback{b})
100	w.last.Visible = false
101	s.relocateCallbacks(root)
102	if len(s.awaiting[suspendedFrameQueue]) != 1 || len(s.awaiting["DP-2"]) != 0 || len(s.awaiting[""]) != 1 || s.frameOutput(child) != suspendedFrameQueue {
103		t.Fatalf("migration: %+v", s.awaiting)
104	}
105	now := time.Unix(100, 0)
106	fire, _, _ := s.dueFrames(now, nil)
107	if !slices.Contains(fire, suspendedFrameQueue) || !slices.Contains(fire, "") {
108		t.Fatalf("initial fire: %v", fire)
109	}
110	delete(s.awaiting, suspendedFrameQueue)
111	delete(s.awaiting, "")
112	s.queueFrames(child, []*wayland.Callback{a})
113	s.queueFrames(cursor, []*wayland.Callback{b})
114	fire, wait, _ := s.dueFrames(now.Add(20*time.Millisecond), nil)
115	if slices.Contains(fire, suspendedFrameQueue) || !slices.Contains(fire, "") || wait != 980*time.Millisecond {
116		t.Fatalf("20ms: fire %v wait %v", fire, wait)
117	}
118	w.last.Visible = true
119	s.relocateCallbacks(root)
120	fire, _, _ = s.dueFrames(now.Add(21*time.Millisecond), nil)
121	if !slices.Contains(fire, "DP-2") || len(s.awaiting[suspendedFrameQueue]) != 0 {
122		t.Fatalf("resume: fire %v queues %+v", fire, s.awaiting)
123	}
124}
125
126// A window only a capture session draws is physically invisible (no scanout,
127// no presentation feedback) but is not throttled: its callbacks stay on its
128// output's queue and move when Captured changes.
129func TestCapturedWindowKeepsOutputFramePacing(t *testing.T) {
130	out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
131	s := &Server{outputs: []*output{out}, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, frameReady: make(chan struct{}, 1)}
132	w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
133	root := &surface{xdg: &xdgSurface{window: w}}
134	a := new(wayland.Callback)
135	s.queueFrames(root, []*wayland.Callback{a})
136	w.last = ports.ConfigureWindow{Output: "DP-2", Captured: true}
137	s.relocateCallbacks(root)
138	if !s.invisible(root) || s.suspended(root) || s.frameOutput(root) != "DP-2" || len(s.awaiting["DP-2"]) != 1 || len(s.awaiting[suspendedFrameQueue]) != 0 {
139		t.Fatalf("captured: invisible %v suspended %v queues %+v", s.invisible(root), s.suspended(root), s.awaiting)
140	}
141	w.last = ports.ConfigureWindow{Output: "DP-2"}
142	s.relocateCallbacks(root)
143	if !s.suspended(root) || len(s.awaiting[suspendedFrameQueue]) != 1 || len(s.awaiting["DP-2"]) != 0 {
144		t.Fatalf("released: queues %+v", s.awaiting)
145	}
146}

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