overlay_test.go RawHistoryFolder

1package drm
2
3import (
4	"os"
5	"testing"
6	"time"
7
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/stretchr/testify/mock"
10	"golang.org/x/sys/unix"
11)
12
13const pZpos = 30
14
15// overlayOutput is a test output with an overlay plane above the primary.
16func overlayOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
17	o, k, commits := testOutput(t, errs...)
18	props := map[string]uint32{"zpos": pZpos}
19	for n, v := range planeProps {
20		props[n] = v
21	}
22	o.primary.props = props
23	o.stray = []*plane{{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2}}
24	o.primary.formats = nil // the overlay's own formats decide
25	o.pickOverlay()
26	if o.overlay == nil {
27		t.Fatal("no overlay picked")
28	}
29	return o, k, commits
30}
31
32// A 1:1 opaque dmabuf window with nothing above goes on the overlay.
33func overlayScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) {
34	s := ports.Scene{Seq: 1, Scale: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{
35		{ID: 1, Rect: ports.Rect{W: 100, H: 100}},
36		{ID: 2, Rect: ports.Rect{X: 100, W: 100, H: 100}},
37	}}
38	return s, map[ports.WindowID]ports.SurfaceContent{
39		2: {ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Width: 100, Height: 100, Format: fourccXRGB}},
40	}
41}
42
43func TestScanoutWorkspaceClipCannotBeBypassed(t *testing.T) {
44	s, contents := fullscreenScene()
45	s.WorkspaceClip = ports.Rect{X: 50, W: 100, H: 100}
46	if _, reason := scanoutCandidate(s, contents, 200, 100); reason != "workspace_clip" {
47		t.Fatalf("fullscreen bypassed clip: %q", reason)
48	}
49	if fullscreenShown(&s) {
50		t.Fatal("clipped fullscreen counted as output-covering")
51	}
52}
53
54func TestOverlayWorkspaceClip(t *testing.T) {
55	s, contents := overlayScene()
56	s.WorkspaceClip = ports.Rect{X: 50, W: 100, H: 100}
57	if _, _, reason := overlayCandidate(s, contents); reason != "workspace_clip" {
58		t.Fatalf("overflowing workspace overlay: %q", reason)
59	}
60	s.WorkspaceClip = ports.Rect{X: 100, W: 100, H: 100}
61	if win, _, reason := overlayCandidate(s, contents); reason != "" || win.ID != 2 {
62		t.Fatalf("contained workspace overlay: %v %q", win.ID, reason)
63	}
64}
65
66func TestOverlayCandidate(t *testing.T) {
67	s, c := overlayScene()
68	if w, _, reason := overlayCandidate(s, c); reason != "" || w.ID != 2 {
69		t.Fatalf("candidate %v %q", w.ID, reason)
70	}
71	// A rotated output composes: planes are not rotated.
72	rs, rc := overlayScene()
73	rs.Transform = 1
74	if _, _, reason := overlayCandidate(rs, rc); reason != "output_transform" {
75		t.Fatalf("rotated output reason %q", reason)
76	}
77	c[2] = ports.SurfaceContent{ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{Format: fourccNV12}}
78	if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
79		t.Fatalf("YUV overlay reason %q", reason)
80	}
81	// A transformed buffer is composed: the plane would show it unrotated.
82	_, c = overlayScene()
83	rotated := c[2]
84	rotated.Transform = 1
85	c[2] = rotated
86	if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
87		t.Fatalf("transformed overlay reason %q", reason)
88	}
89	_, c = overlayScene()
90	s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}})
91	if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
92		t.Fatalf("reason %q", reason)
93	}
94	s, _ = overlayScene()
95	s.Scale = 1.5
96	if _, _, reason := overlayCandidate(s, c); reason != "scaled" {
97		t.Fatalf("reason %q", reason)
98	}
99	s, _ = overlayScene()
100	s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}}
101	if _, _, reason := overlayCandidate(s, c); reason != "layer_above" {
102		t.Fatalf("reason %q", reason)
103	}
104	// The dim veil is composed under the float: nothing leaves for the plane.
105	s, _ = overlayScene()
106	s.Dim = 0.3
107	if _, _, reason := overlayCandidate(s, c); reason != "dim" {
108		t.Fatalf("dim reason %q", reason)
109	}
110	// A dimmed window (a peeking stashed one) needs its veil composed.
111	s, _ = overlayScene()
112	s.Windows[len(s.Windows)-1].Dim = 0.5
113	if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
114		t.Fatalf("dimmed window reason %q", reason)
115	}
116	// A window with the focus effect needs it composed.
117	s, _ = overlayScene()
118	s.Windows[len(s.Windows)-1].FocusEffect = 0.05
119	if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
120		t.Fatalf("pulsing window reason %q", reason)
121	}
122	// An overview preview is drawn smaller than its buffer: composed.
123	s, _ = overlayScene()
124	s.Windows[len(s.Windows)-1].Preview = 0.5
125	if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
126		t.Fatalf("preview reason %q", reason)
127	}
128	// A viewport crop is composed: the plane would show the whole buffer.
129	s, c = overlayScene()
130	cropped := c[2]
131	cropped.Source = [4]float32{10, 10, 50, 50}
132	c[2] = cropped
133	if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
134		t.Fatalf("cropped overlay reason %q", reason)
135	}
136	// A full-buffer source is no crop.
137	cropped.Source = [4]float32{0, 0, 100, 100}
138	c[2] = cropped
139	if _, _, reason := overlayCandidate(s, c); reason != "" {
140		t.Fatalf("full source reason %q", reason)
141	}
142}
143
144// The overlay is used only on SDR outputs: PQ content there needs the
145// renderer's tone mapping.
146func TestOverlaySkipsPQContent(t *testing.T) {
147	o, _, _ := overlayOutput(t)
148	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
149	s, c := overlayScene()
150	pq := c[2]
151	pq.Color = ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
152	c[2] = pq
153	if ov, rest := o.overlayFrame(s, c); ov.fb != 0 || len(rest.Windows) != 2 {
154		t.Fatalf("PQ overlay %+v rest %+v", ov, rest.Windows)
155	}
156}
157
158// The overlay is chosen and the composed scene leaves its window out;
159// the frame commit carries the plane at the window's place.
160func TestOverlayChosen(t *testing.T) {
161	o, k, commits := overlayOutput(t)
162	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
163	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
164	s, c := overlayScene()
165	ov, rest := o.overlayFrame(s, c)
166	if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 {
167		t.Fatalf("overlay %+v rest %+v", ov, rest.Windows)
168	}
169	if !o.testOverlay(70, ov) {
170		t.Fatal("test refused")
171	}
172	if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
173		t.Fatal(err)
174	}
175	last := (*commits)[len(*commits)-1]
176	if v, _ := last.req.value(tOverlay, pFB); v != 88 {
177		t.Fatalf("overlay fb %d", v)
178	}
179	if v, _ := last.req.value(tOverlay, 14); v != 100 {
180		t.Fatalf("overlay x %d", v)
181	}
182	if o.overlayOn != 9 {
183		t.Fatalf("overlayOn %d", o.overlayOn)
184	}
185}
186
187// A TEST_ONLY refusal composes the window and is cached on the buffer.
188func TestOverlayTestRefusedFallsBack(t *testing.T) {
189	o, k, _ := overlayOutput(t, unix.EINVAL)
190	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
191	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
192	s, c := overlayScene()
193	ov, _ := o.overlayFrame(s, c)
194	if o.testOverlay(70, ov) {
195		t.Fatal("refusal taken")
196	}
197	if o.overlayReason != "overlay_refused" {
198		t.Fatalf("refusal reason %q", o.overlayReason)
199	}
200	if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 {
201		t.Fatal("refused buffer tried again")
202	}
203	// The overlay's refusal does not bar direct scanout of the buffer.
204	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
205	if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" {
206		t.Fatalf("scanout after overlay refusal: %d %q", fb, reason)
207	}
208}
209
210// Popups are drawn above every window: one listed first still blocks the
211// overlay; a bordered window does too.
212func TestOverlayPopupAndBorder(t *testing.T) {
213	s, c := overlayScene()
214	s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...)
215	if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
216		t.Fatalf("popup: %q", reason)
217	}
218	s, _ = overlayScene()
219	s.Separators = []ports.Separator{{Rect: ports.Rect{W: 2, H: 2}}}
220	if _, _, reason := overlayCandidate(s, c); reason != "border" {
221		t.Fatalf("border: %q", reason)
222	}
223}
224
225// A cursor commit refused with the overlay on drops the overlay: the
226// next frame is composed.
227func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
228	o, k, _ := overlayOutput(t, nil, unix.EINVAL)
229	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
230	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
231	s, c := overlayScene()
232	ov, _ := o.overlayFrame(s, c)
233	if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
234		t.Fatal(err)
235	}
236	o.frame.endPending()
237	o.cursor.image = true
238	o.cursor.Move(150, 50)
239	if err := o.commitState(false); err != errOverlayDropped {
240		t.Fatalf("err %v", err)
241	}
242	if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" {
243		t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
244	}
245}
246
247// An explicit-sync window on the overlay: the plane carries a duplicate of
248// the client's acquire fence, closed once the frame is committed.
249func TestOverlayCarriesAcquireFence(t *testing.T) {
250	o, k, commits := overlayOutput(t)
251	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
252	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
253	s, c := overlayScene()
254	r, w, err := os.Pipe()
255	if err != nil {
256		t.Fatal(err)
257	}
258	defer r.Close()
259	defer w.Close()
260	win := c[2]
261	win.Acquire = r
262	c[2] = win
263	ov, _ := o.overlayFrame(s, c)
264	if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
265		t.Fatal("overlay has no duplicate of the acquire fence")
266	}
267	fd := ov.acquire.Fd()
268	if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
269		t.Fatal(err)
270	}
271	ov.close()
272	last := (*commits)[len(*commits)-1]
273	if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
274		t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
275	}
276	if _, err := ov.acquire.Stat(); err == nil {
277		t.Fatal("duplicate left open")
278	}
279}

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