feat/hint-hybrid aed7ca4

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, nil); 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, false, nil, nil); 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, false, nil, nil); reason != "" || win.ID != 2 {
62		t.Fatalf("contained workspace overlay: %v %q", win.ID, reason)
63	}
64}
65
66// A tile cut by a panel (TileInset) is composed: the plane would show it
67// whole, over the panel. A tile wholly under the panel is not drawn, so it
68// neither takes the plane nor blocks the visible tile.
69func TestOverlayTileInset(t *testing.T) {
70	s, contents := overlayScene()
71	s.TileInset = ports.Insets{Bottom: 20}
72	if _, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "tile_clip" {
73		t.Fatalf("tile under a panel on the overlay: %q", reason)
74	}
75	// The next cascade band starts under the panel.
76	s, contents = overlayScene()
77	s.OutputHeight = 120
78	s.TileInset = ports.Insets{Bottom: 20}
79	s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{Y: 100, W: 100, H: 100}})
80	contents[3] = ports.SurfaceContent{ID: 3, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 10, Width: 100, Height: 100, Format: fourccXRGB}}
81	if win, _, _, reason := overlayCandidate(s, contents, false, nil, nil); reason != "" || win.ID != 2 {
82		t.Fatalf("visible tile with one under the panel: %v %q", win.ID, reason)
83	}
84}
85
86// A fullscreen window is scanned out when the only other window is a tile
87// the scene does not draw (no room left by the panels).
88func TestScanoutIgnoresUndrawnTile(t *testing.T) {
89	s, contents := fullscreenScene()
90	s.TileInset = ports.Insets{Bottom: 100}
91	s.Windows = append([]ports.SceneWindow{{ID: 2, Rect: ports.Rect{W: 200, H: 100}}}, s.Windows...)
92	if _, reason := scanoutCandidate(s, contents, 200, 100, nil); reason != "" {
93		t.Fatalf("undrawn tile blocked scanout: %q", reason)
94	}
95}
96
97func TestOverlayCandidate(t *testing.T) {
98	s, c := overlayScene()
99	if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 {
100		t.Fatalf("candidate %v %q", w.ID, reason)
101	}
102	// A rotated output composes: planes are not rotated.
103	rs, rc := overlayScene()
104	rs.Transform = 1
105	if _, _, _, reason := overlayCandidate(rs, rc, false, nil, nil); reason != "output_transform" {
106		t.Fatalf("rotated output reason %q", reason)
107	}
108	c[2] = ports.SurfaceContent{ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{Format: fourccNV12}}
109	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
110		t.Fatalf("YUV overlay reason %q", reason)
111	}
112	// A transformed buffer is composed: the plane would show it unrotated.
113	_, c = overlayScene()
114	rotated := c[2]
115	rotated.Transform = 1
116	c[2] = rotated
117	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
118		t.Fatalf("transformed overlay reason %q", reason)
119	}
120	_, c = overlayScene()
121	s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{X: 150, W: 10, H: 10}})
122	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" {
123		t.Fatalf("reason %q", reason)
124	}
125	s, _ = overlayScene()
126	s.Scale = 1.5
127	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "scaled" {
128		t.Fatalf("reason %q", reason)
129	}
130	s, _ = overlayScene()
131	s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 150, W: 5, H: 5}}}
132	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" {
133		t.Fatalf("reason %q", reason)
134	}
135	// The dim veil is composed under the float: nothing leaves for the plane.
136	s, _ = overlayScene()
137	s.Dim = 0.3
138	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "dim" {
139		t.Fatalf("dim reason %q", reason)
140	}
141	// A dimmed window (a peeking stashed one) needs its veil composed.
142	s, _ = overlayScene()
143	s.Windows[len(s.Windows)-1].Dim = 0.5
144	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
145		t.Fatalf("dimmed window reason %q", reason)
146	}
147	// A fading window needs its translucency composed.
148	s, _ = overlayScene()
149	s.Windows[len(s.Windows)-1].Fade = 0.5
150	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
151		t.Fatalf("fading window reason %q", reason)
152	}
153	// A zoomed (animating) window is drawn smaller than its buffer.
154	s, _ = overlayScene()
155	s.Windows[len(s.Windows)-1].Zoom = 0.9
156	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
157		t.Fatalf("zoomed window reason %q", reason)
158	}
159	// An overview preview is drawn smaller than its buffer: composed.
160	s, _ = overlayScene()
161	s.Windows[len(s.Windows)-1].Preview = 0.5
162	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
163		t.Fatalf("preview reason %q", reason)
164	}
165	// A viewport crop is composed: the plane would show the whole buffer.
166	s, c = overlayScene()
167	cropped := c[2]
168	cropped.Source = [4]float32{10, 10, 50, 50}
169	c[2] = cropped
170	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
171		t.Fatalf("cropped overlay reason %q", reason)
172	}
173	// A full-buffer source is no crop.
174	cropped.Source = [4]float32{0, 0, 100, 100}
175	c[2] = cropped
176	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" {
177		t.Fatalf("full source reason %q", reason)
178	}
179}
180
181// The overlay is used only on SDR outputs: PQ content there needs the
182// renderer's tone mapping.
183func TestOverlaySkipsPQContent(t *testing.T) {
184	o, _, _ := overlayOutput(t)
185	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
186	s, c := overlayScene()
187	pq := c[2]
188	pq.Color = ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
189	c[2] = pq
190	if ov, rest := o.overlayFrame(s, c); ov.fb != 0 || len(rest.Windows) != 2 {
191		t.Fatalf("PQ overlay %+v rest %+v", ov, rest.Windows)
192	}
193}
194
195// The overlay is chosen and the composed scene leaves its window out;
196// the frame commit carries the plane at the window's place.
197func TestOverlayChosen(t *testing.T) {
198	o, k, commits := overlayOutput(t)
199	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
200	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
201	s, c := overlayScene()
202	ov, rest := o.overlayFrame(s, c)
203	if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 {
204		t.Fatalf("overlay %+v rest %+v", ov, rest.Windows)
205	}
206	if !o.testOverlay(70, ov) {
207		t.Fatal("test refused")
208	}
209	if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
210		t.Fatal(err)
211	}
212	last := (*commits)[len(*commits)-1]
213	if v, _ := last.req.value(tOverlay, pFB); v != 88 {
214		t.Fatalf("overlay fb %d", v)
215	}
216	if v, _ := last.req.value(tOverlay, 14); v != 100 {
217		t.Fatalf("overlay x %d", v)
218	}
219	if o.overlayOn != 9 {
220		t.Fatalf("overlayOn %d", o.overlayOn)
221	}
222}
223
224// A TEST_ONLY refusal composes the window and is cached on the buffer.
225func TestOverlayTestRefusedFallsBack(t *testing.T) {
226	o, k, _ := overlayOutput(t, unix.EINVAL)
227	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
228	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
229	s, c := overlayScene()
230	ov, _ := o.overlayFrame(s, c)
231	if o.testOverlay(70, ov) {
232		t.Fatal("refusal taken")
233	}
234	if o.overlayReason != "overlay_refused" {
235		t.Fatalf("refusal reason %q", o.overlayReason)
236	}
237	if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 {
238		t.Fatal("refused buffer tried again")
239	}
240	// The overlay's refusal does not bar direct scanout of the buffer.
241	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
242	if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" {
243		t.Fatalf("scanout after overlay refusal: %d %q", fb, reason)
244	}
245}
246
247// Popups are drawn above every window: one listed first still blocks the
248// overlay; a bordered window does too.
249func TestOverlayPopupAndBorder(t *testing.T) {
250	s, c := overlayScene()
251	s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 5, H: 5}}}, s.Windows...)
252	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" {
253		t.Fatalf("popup: %q", reason)
254	}
255	s, _ = overlayScene()
256	s.Separators = []ports.Separator{{Rect: ports.Rect{X: 100, W: 2, H: 100}}}
257	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" {
258		t.Fatalf("border: %q", reason)
259	}
260	s, _ = overlayScene()
261	s.Border.Width = 2
262	s.Windows[1].Inset = ports.SideLeft
263	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" {
264		t.Fatalf("inset content: %q", reason)
265	}
266}
267
268// A float above the tiles covers the tile lines and the lines of windows
269// listed before it; its own lines and those of later windows still block
270// the overlay.
271func TestOverlayFloatOverLines(t *testing.T) {
272	for _, tc := range []struct {
273		name   string
274		sep    ports.WindowID
275		below  bool
276		reason string
277	}{
278		{"tile line", 0, false, ""},
279		{"earlier window line", 1, false, ""},
280		{"own line", 2, false, "border"},
281		{"later window line", 3, false, "border"},
282		{"tile line over a below float", 0, true, "border"},
283	} {
284		t.Run(tc.name, func(t *testing.T) {
285			s, c := overlayScene()
286			s.Windows[1].Floating, s.Windows[1].Below = true, tc.below
287			s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Floating: true, Rect: ports.Rect{W: 10, H: 10}})
288			s.Separators = []ports.Separator{{Rect: ports.Rect{X: 150, W: 2, H: 100}, Window: tc.sep}}
289			if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != tc.reason {
290				t.Fatalf("reason %q, want %q", reason, tc.reason)
291			}
292		})
293	}
294}
295
296// What is drawn elsewhere on screen stays composed on the primary plane: a
297// bar, a popup, another window's lines or a window that do not touch the
298// candidate leave it on the overlay.
299func TestOverlayIgnoresWhatDoesNotCoverIt(t *testing.T) {
300	s, c := overlayScene()
301	s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerTop, Rect: ports.Rect{W: 100, H: 10}}}
302	s.Windows = append(s.Windows,
303		ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 20}},
304		ports.SceneWindow{ID: 8, Floating: true, Rect: ports.Rect{X: 40, Y: 40, W: 20, H: 20}})
305	s.Separators = []ports.Separator{{Rect: ports.Rect{X: 98, W: 2, H: 100}, Window: 1}}
306	if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 {
307		t.Fatalf("candidate %v %q", w.ID, reason)
308	}
309	// Touching edges do not cover it.
310	s.Layers[0].Rect = ports.Rect{W: 100, H: 100}
311	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" {
312		t.Fatalf("adjacent layer: %q", reason)
313	}
314	s.Layers[0].Rect = ports.Rect{W: 101, H: 10}
315	if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" {
316		t.Fatalf("overlapping layer: %q", reason)
317	}
318}
319
320// Draw order decides: a window listed before the candidate is under it, one
321// after is over it; a popup is over it wherever it is listed, unless hidden.
322func TestOverlayStackingOrder(t *testing.T) {
323	under := ports.SceneWindow{ID: 8, Floating: true, Below: true, Rect: ports.Rect{X: 120, W: 20, H: 20}}
324	over := under
325	over.Below = false
326	popup := ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 20, H: 20}}
327	for name, tc := range map[string]struct {
328		edit func(*ports.Scene)
329		want string
330	}{
331		"window under": {func(s *ports.Scene) { s.Windows = append([]ports.SceneWindow{under}, s.Windows...) }, ""},
332		"window over":  {func(s *ports.Scene) { s.Windows = append(s.Windows, over) }, "window_above"},
333		"popup after":  {func(s *ports.Scene) { s.Windows = append(s.Windows, popup) }, "window_above"},
334		"hidden popup": {func(s *ports.Scene) { p := popup; p.Hidden = true; s.Windows = append(s.Windows, p) }, ""},
335		"popup touching": {func(s *ports.Scene) {
336			p := popup
337			p.Rect.X = 80
338			s.Windows = append(s.Windows, p)
339		}, ""},
340	} {
341		s, c := overlayScene()
342		tc.edit(&s)
343		if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != tc.want {
344			t.Errorf("%s: reason %q, want %q", name, reason, tc.want)
345		}
346	}
347}
348
349// A cursor commit refused with the overlay on drops the overlay: the
350// next frame is composed.
351func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
352	o, k, _ := overlayOutput(t, nil, unix.EINVAL)
353	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
354	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
355	s, c := overlayScene()
356	ov, _ := o.overlayFrame(s, c)
357	if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
358		t.Fatal(err)
359	}
360	o.frame.endPending()
361	o.cursor.image = true
362	o.cursor.Move(150, 50)
363	if err := o.commitState(false); err != errOverlayDropped {
364		t.Fatalf("err %v", err)
365	}
366	if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" {
367		t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
368	}
369}
370
371// An explicit-sync window on the overlay: the plane carries a duplicate of
372// the client's acquire fence, closed once the frame is committed.
373func TestOverlayCarriesAcquireFence(t *testing.T) {
374	o, k, commits := overlayOutput(t)
375	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
376	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
377	s, c := overlayScene()
378	r, w, err := os.Pipe()
379	if err != nil {
380		t.Fatal(err)
381	}
382	defer r.Close()
383	defer w.Close()
384	win := c[2]
385	win.Acquire = r
386	c[2] = win
387	ov, _ := o.overlayFrame(s, c)
388	if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
389		t.Fatal("overlay has no duplicate of the acquire fence")
390	}
391	fd := ov.acquire.Fd()
392	if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
393		t.Fatal(err)
394	}
395	ov.close()
396	last := (*commits)[len(*commits)-1]
397	if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
398		t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
399	}
400	if _, err := ov.acquire.Stat(); err == nil {
401		t.Fatal("duplicate left open")
402	}
403}

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