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 // A window with the focus effect needs it composed.
160 s, _ = overlayScene()
161 s.Windows[len(s.Windows)-1].FocusEffect = 0.05
162 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
163 t.Fatalf("pulsing window reason %q", reason)
164 }
165 // An overview preview is drawn smaller than its buffer: composed.
166 s, _ = overlayScene()
167 s.Windows[len(s.Windows)-1].Preview = 0.5
168 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
169 t.Fatalf("preview reason %q", reason)
170 }
171 // A viewport crop is composed: the plane would show the whole buffer.
172 s, c = overlayScene()
173 cropped := c[2]
174 cropped.Source = [4]float32{10, 10, 50, 50}
175 c[2] = cropped
176 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "no_candidate" {
177 t.Fatalf("cropped overlay reason %q", reason)
178 }
179 // A full-buffer source is no crop.
180 cropped.Source = [4]float32{0, 0, 100, 100}
181 c[2] = cropped
182 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" {
183 t.Fatalf("full source reason %q", reason)
184 }
185}
186
187// The overlay is used only on SDR outputs: PQ content there needs the
188// renderer's tone mapping.
189func TestOverlaySkipsPQContent(t *testing.T) {
190 o, _, _ := overlayOutput(t)
191 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
192 s, c := overlayScene()
193 pq := c[2]
194 pq.Color = ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
195 c[2] = pq
196 if ov, rest := o.overlayFrame(s, c); ov.fb != 0 || len(rest.Windows) != 2 {
197 t.Fatalf("PQ overlay %+v rest %+v", ov, rest.Windows)
198 }
199}
200
201// The overlay is chosen and the composed scene leaves its window out;
202// the frame commit carries the plane at the window's place.
203func TestOverlayChosen(t *testing.T) {
204 o, k, commits := overlayOutput(t)
205 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
206 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
207 s, c := overlayScene()
208 ov, rest := o.overlayFrame(s, c)
209 if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 {
210 t.Fatalf("overlay %+v rest %+v", ov, rest.Windows)
211 }
212 if !o.testOverlay(70, ov) {
213 t.Fatal("test refused")
214 }
215 if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
216 t.Fatal(err)
217 }
218 last := (*commits)[len(*commits)-1]
219 if v, _ := last.req.value(tOverlay, pFB); v != 88 {
220 t.Fatalf("overlay fb %d", v)
221 }
222 if v, _ := last.req.value(tOverlay, 14); v != 100 {
223 t.Fatalf("overlay x %d", v)
224 }
225 if o.overlayOn != 9 {
226 t.Fatalf("overlayOn %d", o.overlayOn)
227 }
228}
229
230// A TEST_ONLY refusal composes the window and is cached on the buffer.
231func TestOverlayTestRefusedFallsBack(t *testing.T) {
232 o, k, _ := overlayOutput(t, unix.EINVAL)
233 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
234 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
235 s, c := overlayScene()
236 ov, _ := o.overlayFrame(s, c)
237 if o.testOverlay(70, ov) {
238 t.Fatal("refusal taken")
239 }
240 if o.overlayReason != "overlay_refused" {
241 t.Fatalf("refusal reason %q", o.overlayReason)
242 }
243 if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 {
244 t.Fatal("refused buffer tried again")
245 }
246 // The overlay's refusal does not bar direct scanout of the buffer.
247 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
248 if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" {
249 t.Fatalf("scanout after overlay refusal: %d %q", fb, reason)
250 }
251}
252
253// Popups are drawn above every window: one listed first still blocks the
254// overlay; a bordered window does too.
255func TestOverlayPopupAndBorder(t *testing.T) {
256 s, c := overlayScene()
257 s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 5, H: 5}}}, s.Windows...)
258 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "window_above" {
259 t.Fatalf("popup: %q", reason)
260 }
261 s, _ = overlayScene()
262 s.Separators = []ports.Separator{{Rect: ports.Rect{X: 100, W: 2, H: 100}}}
263 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" {
264 t.Fatalf("border: %q", reason)
265 }
266 s, _ = overlayScene()
267 s.Border.Width = 2
268 s.Windows[1].Inset = ports.SideLeft
269 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "border" {
270 t.Fatalf("inset content: %q", reason)
271 }
272}
273
274// A float above the tiles covers the tile lines and the lines of windows
275// listed before it; its own lines and those of later windows still block
276// the overlay.
277func TestOverlayFloatOverLines(t *testing.T) {
278 for _, tc := range []struct {
279 name string
280 sep ports.WindowID
281 below bool
282 reason string
283 }{
284 {"tile line", 0, false, ""},
285 {"earlier window line", 1, false, ""},
286 {"own line", 2, false, "border"},
287 {"later window line", 3, false, "border"},
288 {"tile line over a below float", 0, true, "border"},
289 } {
290 t.Run(tc.name, func(t *testing.T) {
291 s, c := overlayScene()
292 s.Windows[1].Floating, s.Windows[1].Below = true, tc.below
293 s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Floating: true, Rect: ports.Rect{W: 10, H: 10}})
294 s.Separators = []ports.Separator{{Rect: ports.Rect{X: 150, W: 2, H: 100}, Window: tc.sep}}
295 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != tc.reason {
296 t.Fatalf("reason %q, want %q", reason, tc.reason)
297 }
298 })
299 }
300}
301
302// What is drawn elsewhere on screen stays composed on the primary plane: a
303// bar, a popup, another window's lines or a window that do not touch the
304// candidate leave it on the overlay.
305func TestOverlayIgnoresWhatDoesNotCoverIt(t *testing.T) {
306 s, c := overlayScene()
307 s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerTop, Rect: ports.Rect{W: 100, H: 10}}}
308 s.Windows = append(s.Windows,
309 ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 20}},
310 ports.SceneWindow{ID: 8, Floating: true, Rect: ports.Rect{X: 40, Y: 40, W: 20, H: 20}})
311 s.Separators = []ports.Separator{{Rect: ports.Rect{X: 98, W: 2, H: 100}, Window: 1}}
312 if w, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" || w.ID != 2 {
313 t.Fatalf("candidate %v %q", w.ID, reason)
314 }
315 // Touching edges do not cover it.
316 s.Layers[0].Rect = ports.Rect{W: 100, H: 100}
317 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "" {
318 t.Fatalf("adjacent layer: %q", reason)
319 }
320 s.Layers[0].Rect = ports.Rect{W: 101, H: 10}
321 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != "layer_above" {
322 t.Fatalf("overlapping layer: %q", reason)
323 }
324}
325
326// Draw order decides: a window listed before the candidate is under it, one
327// after is over it; a popup is over it wherever it is listed, unless hidden.
328func TestOverlayStackingOrder(t *testing.T) {
329 under := ports.SceneWindow{ID: 8, Floating: true, Below: true, Rect: ports.Rect{X: 120, W: 20, H: 20}}
330 over := under
331 over.Below = false
332 popup := ports.SceneWindow{ID: 7, Popup: true, Rect: ports.Rect{X: 120, W: 20, H: 20}}
333 for name, tc := range map[string]struct {
334 edit func(*ports.Scene)
335 want string
336 }{
337 "window under": {func(s *ports.Scene) { s.Windows = append([]ports.SceneWindow{under}, s.Windows...) }, ""},
338 "window over": {func(s *ports.Scene) { s.Windows = append(s.Windows, over) }, "window_above"},
339 "popup after": {func(s *ports.Scene) { s.Windows = append(s.Windows, popup) }, "window_above"},
340 "hidden popup": {func(s *ports.Scene) { p := popup; p.Hidden = true; s.Windows = append(s.Windows, p) }, ""},
341 "popup touching": {func(s *ports.Scene) {
342 p := popup
343 p.Rect.X = 80
344 s.Windows = append(s.Windows, p)
345 }, ""},
346 } {
347 s, c := overlayScene()
348 tc.edit(&s)
349 if _, _, _, reason := overlayCandidate(s, c, false, nil, nil); reason != tc.want {
350 t.Errorf("%s: reason %q, want %q", name, reason, tc.want)
351 }
352 }
353}
354
355// A cursor commit refused with the overlay on drops the overlay: the
356// next frame is composed.
357func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
358 o, k, _ := overlayOutput(t, nil, unix.EINVAL)
359 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
360 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
361 s, c := overlayScene()
362 ov, _ := o.overlayFrame(s, c)
363 if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
364 t.Fatal(err)
365 }
366 o.frame.endPending()
367 o.cursor.image = true
368 o.cursor.Move(150, 50)
369 if err := o.commitState(false); err != errOverlayDropped {
370 t.Fatalf("err %v", err)
371 }
372 if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" {
373 t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
374 }
375}
376
377// An explicit-sync window on the overlay: the plane carries a duplicate of
378// the client's acquire fence, closed once the frame is committed.
379func TestOverlayCarriesAcquireFence(t *testing.T) {
380 o, k, commits := overlayOutput(t)
381 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
382 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
383 s, c := overlayScene()
384 r, w, err := os.Pipe()
385 if err != nil {
386 t.Fatal(err)
387 }
388 defer r.Close()
389 defer w.Close()
390 win := c[2]
391 win.Acquire = r
392 c[2] = win
393 ov, _ := o.overlayFrame(s, c)
394 if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
395 t.Fatal("overlay has no duplicate of the acquire fence")
396 }
397 fd := ov.acquire.Fd()
398 if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
399 t.Fatal(err)
400 }
401 ov.close()
402 last := (*commits)[len(*commits)-1]
403 if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
404 t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
405 }
406 if _, err := ov.acquire.Stat(); err == nil {
407 t.Fatal("duplicate left open")
408 }
409}