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 c[2] = ports.SurfaceContent{ID: 2, Width: 100, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{Format: fourccNV12}}
72 if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
73 t.Fatalf("YUV overlay reason %q", reason)
74 }
75 // A transformed buffer is composed: the plane would show it unrotated.
76 _, c = overlayScene()
77 rotated := c[2]
78 rotated.Transform = 1
79 c[2] = rotated
80 if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
81 t.Fatalf("transformed overlay reason %q", reason)
82 }
83 _, c = overlayScene()
84 s.Windows = append(s.Windows, ports.SceneWindow{ID: 3, Rect: ports.Rect{W: 10, H: 10}})
85 if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
86 t.Fatalf("reason %q", reason)
87 }
88 s, _ = overlayScene()
89 s.Scale = 1.5
90 if _, _, reason := overlayCandidate(s, c); reason != "scaled" {
91 t.Fatalf("reason %q", reason)
92 }
93 s, _ = overlayScene()
94 s.Layers = []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 5, H: 5}}}
95 if _, _, reason := overlayCandidate(s, c); reason != "layer_above" {
96 t.Fatalf("reason %q", reason)
97 }
98 // The dim veil is composed under the float: nothing leaves for the plane.
99 s, _ = overlayScene()
100 s.Dim = 0.3
101 if _, _, reason := overlayCandidate(s, c); reason != "dim" {
102 t.Fatalf("dim reason %q", reason)
103 }
104 // A dimmed window (a peeking stashed one) needs its veil composed.
105 s, _ = overlayScene()
106 s.Windows[len(s.Windows)-1].Dim = 0.5
107 if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
108 t.Fatalf("dimmed window reason %q", reason)
109 }
110 // A window with the focus effect needs it composed.
111 s, _ = overlayScene()
112 s.Windows[len(s.Windows)-1].FocusEffect = 0.05
113 if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
114 t.Fatalf("pulsing window reason %q", reason)
115 }
116 // An overview preview is drawn smaller than its buffer: composed.
117 s, _ = overlayScene()
118 s.Windows[len(s.Windows)-1].Preview = 0.5
119 if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
120 t.Fatalf("preview reason %q", reason)
121 }
122 // A viewport crop is composed: the plane would show the whole buffer.
123 s, c = overlayScene()
124 cropped := c[2]
125 cropped.Source = [4]float32{10, 10, 50, 50}
126 c[2] = cropped
127 if _, _, reason := overlayCandidate(s, c); reason != "no_candidate" {
128 t.Fatalf("cropped overlay reason %q", reason)
129 }
130 // A full-buffer source is no crop.
131 cropped.Source = [4]float32{0, 0, 100, 100}
132 c[2] = cropped
133 if _, _, reason := overlayCandidate(s, c); reason != "" {
134 t.Fatalf("full source reason %q", reason)
135 }
136}
137
138// The overlay is used only on SDR outputs: PQ content there needs the
139// renderer's tone mapping.
140func TestOverlaySkipsPQContent(t *testing.T) {
141 o, _, _ := overlayOutput(t)
142 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
143 s, c := overlayScene()
144 pq := c[2]
145 pq.Color = ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
146 c[2] = pq
147 if ov, rest := o.overlayFrame(s, c); ov.fb != 0 || len(rest.Windows) != 2 {
148 t.Fatalf("PQ overlay %+v rest %+v", ov, rest.Windows)
149 }
150}
151
152// The overlay is chosen and the composed scene leaves its window out;
153// the frame commit carries the plane at the window's place.
154func TestOverlayChosen(t *testing.T) {
155 o, k, commits := overlayOutput(t)
156 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
157 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
158 s, c := overlayScene()
159 ov, rest := o.overlayFrame(s, c)
160 if ov.fb != 88 || len(rest.Windows) != 1 || rest.Windows[0].ID != 1 {
161 t.Fatalf("overlay %+v rest %+v", ov, rest.Windows)
162 }
163 if !o.testOverlay(70, ov) {
164 t.Fatal("test refused")
165 }
166 if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
167 t.Fatal(err)
168 }
169 last := (*commits)[len(*commits)-1]
170 if v, _ := last.req.value(tOverlay, pFB); v != 88 {
171 t.Fatalf("overlay fb %d", v)
172 }
173 if v, _ := last.req.value(tOverlay, 14); v != 100 {
174 t.Fatalf("overlay x %d", v)
175 }
176 if o.overlayOn != 9 {
177 t.Fatalf("overlayOn %d", o.overlayOn)
178 }
179}
180
181// A TEST_ONLY refusal composes the window and is cached on the buffer.
182func TestOverlayTestRefusedFallsBack(t *testing.T) {
183 o, k, _ := overlayOutput(t, unix.EINVAL)
184 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
185 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
186 s, c := overlayScene()
187 ov, _ := o.overlayFrame(s, c)
188 if o.testOverlay(70, ov) {
189 t.Fatal("refusal taken")
190 }
191 if o.overlayReason != "overlay_refused" {
192 t.Fatalf("refusal reason %q", o.overlayReason)
193 }
194 if ov2, rest := o.overlayFrame(s, c); ov2.fb != 0 || len(rest.Windows) != 2 {
195 t.Fatal("refused buffer tried again")
196 }
197 // The overlay's refusal does not bar direct scanout of the buffer.
198 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
199 if fb, reason := o.scanoutFB(c[2].DMABuf, time.Now()); fb != 88 || reason != "" {
200 t.Fatalf("scanout after overlay refusal: %d %q", fb, reason)
201 }
202}
203
204// Popups are drawn above every window: one listed first still blocks the
205// overlay; a bordered window does too.
206func TestOverlayPopupAndBorder(t *testing.T) {
207 s, c := overlayScene()
208 s.Windows = append([]ports.SceneWindow{{ID: 7, Popup: true, Rect: ports.Rect{W: 5, H: 5}}}, s.Windows...)
209 if _, _, reason := overlayCandidate(s, c); reason != "window_above" {
210 t.Fatalf("popup: %q", reason)
211 }
212 s, _ = overlayScene()
213 s.Separators = []ports.Separator{{Rect: ports.Rect{W: 2, H: 2}}}
214 if _, _, reason := overlayCandidate(s, c); reason != "border" {
215 t.Fatalf("border: %q", reason)
216 }
217}
218
219// A cursor commit refused with the overlay on drops the overlay: the
220// next frame is composed.
221func TestOverlayCursorConflictDropsOverlay(t *testing.T) {
222 o, k, _ := overlayOutput(t, nil, unix.EINVAL)
223 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
224 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
225 s, c := overlayScene()
226 ov, _ := o.overlayFrame(s, c)
227 if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
228 t.Fatal(err)
229 }
230 o.frame.endPending()
231 o.cursor.image = true
232 o.cursor.Move(150, 50)
233 if err := o.commitState(false); err != errOverlayDropped {
234 t.Fatalf("err %v", err)
235 }
236 if ov2, _ := o.overlayFrame(s, c); ov2.fb != 0 || o.overlayReason != "cursor_conflict" {
237 t.Fatalf("overlay kept: %+v %q", ov2, o.overlayReason)
238 }
239}
240
241// An explicit-sync window on the overlay: the plane carries a duplicate of
242// the client's acquire fence, closed once the frame is committed.
243func TestOverlayCarriesAcquireFence(t *testing.T) {
244 o, k, commits := overlayOutput(t)
245 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
246 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
247 s, c := overlayScene()
248 r, w, err := os.Pipe()
249 if err != nil {
250 t.Fatal(err)
251 }
252 defer r.Close()
253 defer w.Close()
254 win := c[2]
255 win.Acquire = r
256 c[2] = win
257 ov, _ := o.overlayFrame(s, c)
258 if ov.acquire == nil || ov.acquire.Fd() == r.Fd() {
259 t.Fatal("overlay has no duplicate of the acquire fence")
260 }
261 fd := ov.acquire.Fd()
262 if err := o.commitWith(70, nil, false, false, pendingFrame{queued: ov.buf}, ov); err != nil {
263 t.Fatal(err)
264 }
265 ov.close()
266 last := (*commits)[len(*commits)-1]
267 if v, ok := last.req.value(tOverlay, pFence); !ok || v != uint64(fd) {
268 t.Fatalf("overlay IN_FENCE_FD %d %v, want %d", v, ok, fd)
269 }
270 if _, err := ov.acquire.Stat(); err == nil {
271 t.Fatal("duplicate left open")
272 }
273}