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)
11
12func TestScanoutCandidate(t *testing.T) {
13 full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
14 scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}}
15 buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccARGB, Planes: []ports.DMABufPlane{{}}}
16 game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf}
17 for _, tc := range []struct {
18 name string
19 scene func(s *ports.Scene)
20 cont func(c *ports.SurfaceContent)
21 reason string
22 }{
23 {"scanout", nil, nil, ""},
24 {"hidden others ignored", func(s *ports.Scene) {
25 s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Hidden: true})
26 }, nil, ""},
27 {"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"},
28 {"rotated output", func(s *ports.Scene) { s.Transform = 1 }, nil, "output_transform"},
29 {"dimmed", func(s *ports.Scene) { s.Windows[0].Dim = 0.5 }, nil, "dim"},
30 {"window fade", func(s *ports.Scene) { s.Windows[0].Fade = 0.5 }, nil, "window_fade"},
31 {"window preview", func(s *ports.Scene) { s.Windows[0].Preview = 0.9 }, nil, "window_fade"},
32 {"window zoom", func(s *ports.Scene) { s.Windows[0].Zoom = 0.9 }, nil, "window_fade"},
33 {"faded", nil, func(c *ports.SurfaceContent) { c.Fade = 0.5 }, "fade"},
34 {"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"},
35 {"float above", func(s *ports.Scene) {
36 s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}})
37 }, nil, "other_windows"},
38 {"not covering", func(s *ports.Scene) { s.Windows[0].Rect.W = 90 }, nil, "other_windows"},
39 {"overlay layer", func(s *ports.Scene) {
40 s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}}
41 }, nil, "overlay_surface"},
42 {"top layer", func(s *ports.Scene) {
43 s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerTop, Rect: ports.Rect{W: 10, H: 10}}}
44 }, nil, "overlay_surface"},
45 {"background layer", func(s *ports.Scene) {
46 s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerBackground, Rect: ports.Rect{W: 10, H: 10}}}
47 }, nil, ""},
48 {"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"},
49 {"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"},
50 {"buffer transform", nil, func(c *ports.SurfaceContent) { c.Transform = 2 }, "buffer_transform"},
51 {"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, ""},
52 {"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"},
53 {"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "letterbox"},
54 {"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""},
55 {"wine client subsurface", nil, wineTree(nil), ""},
56 {"client subsurface translucent", nil, wineTree(func(ch *ports.Subsurface) { ch.Opaque = false }), "subsurface_translucent"},
57 {"client subsurface shm", nil, wineTree(func(ch *ports.Subsurface) { ch.DMABuf, ch.SHM = nil, &ports.SHMBuffer{} }), "not_dmabuf"},
58 {"client subsurface offset", nil, wineTree(func(ch *ports.Subsurface) { ch.X = 4 }), "subsurface_offset"},
59 {"client subsurface below root", nil, wineTree(func(ch *ports.Subsurface) { ch.Below = true }), "subsurface_below"},
60 {"client subsurface cropped", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 100, 50} }), ""},
61 {"client subsurface full source", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 200, 100} }), ""},
62 {"root cropped", nil, func(c *ports.SurfaceContent) { c.Source = [4]float32{10, 0, 190, 100} }, ""},
63 {"client subsurface scaled", nil, wineTree(func(ch *ports.Subsurface) { ch.Width = 100 }), ""},
64 {"client subsurface small", nil, wineTree(func(ch *ports.Subsurface) { ch.LogicalW = 90 }), "letterbox"},
65 {"two subsurfaces", nil, func(c *ports.SurfaceContent) {
66 wineTree(nil)(c)
67 c.Children = append(c.Children, c.Children[0])
68 }, "subsurfaces"},
69 } {
70 s := scene
71 s.Windows = append([]ports.SceneWindow(nil), scene.Windows...)
72 c := game
73 if tc.scene != nil {
74 tc.scene(&s)
75 }
76 if tc.cont != nil {
77 tc.cont(&c)
78 }
79 if _, reason := scanoutCandidate(s, map[ports.WindowID]ports.SurfaceContent{1: c}, 200, 100, nil); reason != tc.reason {
80 t.Errorf("%s: reason %q, want %q", tc.name, reason, tc.reason)
81 }
82 }
83}
84
85// wineTree turns the test content into Wine's layout: the root is a wl_shm
86// frame and the game is one opaque GPU subsurface over the client area.
87func wineTree(edit func(ch *ports.Subsurface)) func(c *ports.SurfaceContent) {
88 return func(c *ports.SurfaceContent) {
89 ch := ports.Subsurface{SurfaceContent: *c}
90 ch.Opaque = true
91 if edit != nil {
92 edit(&ch)
93 }
94 c.DMABuf, c.SHM = nil, &ports.SHMBuffer{}
95 c.Children = []ports.Subsurface{ch}
96 }
97}
98
99// Wine's game subsurface is scanned out in place of the window: the
100// content keeps the window's ID and Seq and takes the subsurface buffer.
101func TestScanoutCandidateClientSubsurface(t *testing.T) {
102 scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}}}
103 game := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccXRGB, Planes: []ports.DMABufPlane{{}}}
104 acquire := &os.File{}
105 root := ports.SurfaceContent{ID: 1, Seq: 7, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, SHM: &ports.SHMBuffer{},
106 Geometry: ports.Rect{W: 100, H: 50},
107 Children: []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Seq: 3, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, Opaque: true, DMABuf: game, Async: true, Acquire: acquire}}}}
108 c, reason := scanoutCandidate(scene, map[ports.WindowID]ports.SurfaceContent{1: root}, 200, 100, nil)
109 if reason != "" || c.DMABuf != game || c.ID != 1 || c.Seq != 7 || !c.Async || c.Acquire != acquire || c.Geometry != (ports.Rect{}) {
110 t.Fatalf("candidate %+v, reason %q", c, reason)
111 }
112
113 // A single-pixel root is hidden by the opaque dmabuf over it, like an shm
114 // one; alone it is not a dmabuf.
115 solidRoot := root
116 solidRoot.SHM, solidRoot.Solid = nil, &ports.SolidColor{A: 1}
117 c, reason = scanoutCandidate(scene, map[ports.WindowID]ports.SurfaceContent{1: solidRoot}, 200, 100, nil)
118 if reason != "" || c.DMABuf != game || c.Solid != nil || c.ID != 1 || c.Seq != 7 {
119 t.Fatalf("solid root with dmabuf child: candidate %+v, reason %q", c, reason)
120 }
121 solidRoot.Children = nil
122 if _, reason = scanoutCandidate(scene, map[ports.WindowID]ports.SurfaceContent{1: solidRoot}, 200, 100, nil); reason != "not_dmabuf" {
123 t.Fatalf("solid root alone: reason %q, want not_dmabuf", reason)
124 }
125}
126
127// VRR follows a visible fullscreen window covering the output, even
128// when other windows or layers force composition.
129func TestFullscreenShown(t *testing.T) {
130 full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
131 for _, tc := range []struct {
132 name string
133 wins []ports.SceneWindow
134 want bool
135 }{
136 {"fullscreen", []ports.SceneWindow{full}, true},
137 {"with dialog above", []ports.SceneWindow{full, {ID: 2, Rect: ports.Rect{W: 10, H: 10}, Floating: true}}, true},
138 {"hidden", []ports.SceneWindow{{ID: 1, Rect: full.Rect, Fullscreen: true, Hidden: true}}, false},
139 {"off screen", []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 100, W: 100, H: 50}, Fullscreen: true}}, false},
140 {"tiled covering", []ports.SceneWindow{{ID: 1, Rect: full.Rect}}, false},
141 {"none", nil, false},
142 } {
143 s := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: tc.wins}
144 if got := fullscreenShown(&s); got != tc.want {
145 t.Errorf("%s: %v, want %v", tc.name, got, tc.want)
146 }
147 }
148}
149
150// fullscreenShown runs on every composed frame of a VRR output.
151func TestFullscreenShownAllocations(t *testing.T) {
152 s := tiledFullscreenScene()
153 if allocs := testing.AllocsPerRun(100, func() { fullscreenShown(&s) }); allocs != 0 {
154 t.Errorf("fullscreenShown: %.1f allocs, want 0", allocs)
155 }
156}
157
158func BenchmarkFullscreenShown(b *testing.B) {
159 s := tiledFullscreenScene()
160 b.ReportAllocs()
161 for b.Loop() {
162 fullscreenShown(&s)
163 }
164}
165
166// tiledFullscreenScene is 64 tiled windows with the fullscreen one last
167// (worst case for the walk).
168func tiledFullscreenScene() ports.Scene {
169 s := ports.Scene{OutputWidth: 3840, OutputHeight: 2160}
170 for i := range 64 {
171 s.Windows = append(s.Windows, ports.SceneWindow{ID: ports.WindowID(i + 1), Rect: ports.Rect{X: i * 60, W: 60, H: 2160}})
172 }
173 s.Windows = append(s.Windows, ports.SceneWindow{ID: 99, Rect: ports.Rect{W: 3840, H: 2160}, Fullscreen: true})
174 return s
175}
176
177// A buffer scans out when the primary plane lists its format (or the
178// opaque variant) with its modifier; the tranche offers only formats the
179// renderer also composes.
180func TestScanoutFormatFollowsInFormats(t *testing.T) {
181 const tiled = 0x0200000000000001
182 o, k, _ := testOutput(t)
183 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0}, {Format: fourccXR30, Modifier: tiled}}
184 nv12 := uint32('N' | 'V'<<8 | '1'<<16 | '2'<<24)
185 for _, tc := range []struct {
186 name string
187 format uint32
188 mod uint64
189 as uint32
190 ok bool
191 }{
192 {"argb as xrgb", fourccARGB, tiled, fourccXRGB, true},
193 {"xrgb linear", fourccXRGB, 0, fourccXRGB, true},
194 {"10 bit alpha as opaque", fourccAR30, tiled, fourccXR30, true},
195 {"modifier not listed", fourccXRGB, 0x0200000000000002, 0, false},
196 {"format not listed", nv12, tiled, 0, false},
197 } {
198 got, ok := o.scanoutFormat(&ports.DMABuf{Format: tc.format, Modifier: tc.mod})
199 if ok != tc.ok || ok && got != tc.as {
200 t.Errorf("%s: %#x %v", tc.name, got, ok)
201 }
202 }
203 sampled := []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0x0200000000000002}, {Format: nv12, Modifier: tiled}}
204 if got := o.scanoutFormats(sampled); len(got) != 1 || got[0] != sampled[0] {
205 t.Fatalf("tranche %v", got)
206 }
207 // A buffer not in IN_FORMATS is refused before any import.
208 o.clientFBs = map[uint64]*clientFB{}
209 if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 4, Format: nv12, Modifier: tiled}, time.Now()); fb != 0 || reason != "format" {
210 t.Fatalf("fb %d reason %q", fb, reason)
211 }
212 // A listed one is imported with every plane (addFB builds ADDFB2).
213 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
214 if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 5, Format: fourccARGB, Modifier: tiled, Planes: []ports.DMABufPlane{{}, {}}}, time.Now()); fb != 88 || reason != "" {
215 t.Fatalf("fb %d reason %q", fb, reason)
216 }
217}
218
219// Each modeset reports the formats clients may allocate for scanout;
220// with direct scanout off the list is empty.
221func TestModesetSendsScanoutFormats(t *testing.T) {
222 const tiled = 0x0200000000000001
223 for _, on := range []bool{true, false} {
224 o, k, _ := testOutput(t)
225 o.cursor = nil
226 o.scanout, o.device = on, 9
227 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccNV12, Modifier: tiled}, {Format: fourccP010, Modifier: tiled}}
228 o.sampled = []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0}, {Format: fourccNV12, Modifier: tiled}, {Format: fourccP010, Modifier: tiled}}
229 ch := make(chan ports.OutputFormats, 1)
230 o.formats = ch
231 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
232 if err := o.modeset(); err != nil {
233 t.Fatal(err)
234 }
235 f := <-ch
236 if f.Output != "DP-1" || f.Device != 9 {
237 t.Fatalf("report %+v", f)
238 }
239 if on != (len(f.Formats) == 1 && f.Formats[0] == o.sampled[0]) || !on && len(f.Formats) != 0 {
240 t.Fatalf("scanout=%v formats %v", on, f.Formats)
241 }
242 }
243}
244
245func TestHDRScanoutDecision(t *testing.T) {
246 o, k, _ := testOutput(t)
247 o.hdr.on = true
248 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30}}
249 scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Fullscreen: true, Rect: ports.Rect{W: 200, H: 100}}}}
250 c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, DMABuf: &ports.DMABuf{ID: 11, Format: fourccXR30}}
251 check := func(want string) {
252 t.Helper()
253 fb, _, _ := o.scanoutFrame(scene, map[ports.WindowID]ports.SurfaceContent{1: c})
254 if o.reason != want || (fb != 0) != (want == "") {
255 t.Fatalf("fb %d reason %q, want %q", fb, o.reason, want)
256 }
257 }
258 check("hdr_sdr_content")
259 c.Color = ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
260 c.DMABuf.Format = fourccXRGB
261 check("hdr_format")
262 c.DMABuf.Format = fourccP010
263 check("yuv")
264 c.DMABuf.Format = fourccXR30
265 k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(uint32(77), nil).Once()
266 check("")
267 o.hdr.on = false
268 check("sdr_pq_content")
269}
270
271func TestModesetReportsConfirmedHDR(t *testing.T) {
272 o, k, _ := testOutput(t)
273 o.cursor = nil
274 o.hdr.on = true
275 o.hdr.cap = hdrCapability{MaxLuminance: 1000, MaxFrameAverage: 400, MinLuminance: 0.005}
276 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30}, {Format: fourccXRGB}}
277 o.sampled = []ports.DMABufFormat{{Format: fourccXR30}, {Format: fourccXRGB}}
278 ch := make(chan ports.OutputFormats, 2)
279 o.formats = ch
280 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Twice()
281 k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
282 if err := o.modeset(); err != nil {
283 t.Fatal(err)
284 }
285 if f := <-ch; f.HDR == nil || f.HDR.MaxLuminance != 1000 || f.HDR.MaxFrameAverage != 400 || f.HDR.MinLuminance != 0.005 || len(f.Formats) != 1 || f.Formats[0].Format != fourccXR30 {
286 t.Fatalf("HDR report: %+v", f)
287 }
288 o.hdr.on = false // fallback to SDR
289 if err := o.modeset(); err != nil {
290 t.Fatal(err)
291 }
292 if f := <-ch; f.HDR != nil || len(f.Formats) != 1 || f.Formats[0].Format != fourccXRGB {
293 t.Fatalf("SDR fallback report: %+v", f)
294 }
295}
296
297// Power off withdraws HDR and scanout formats: clients must not target an
298// inactive output.
299func TestPowerOffWithdrawsHDRAndFormats(t *testing.T) {
300 o, _, _ := testOutput(t)
301 o.cursor = nil
302 o.hdr.on, o.scanout = true, true
303 o.hdr.cap = hdrCapability{MaxLuminance: 1000}
304 o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30}}
305 o.sampled = []ports.DMABufFormat{{Format: fourccXR30}}
306 ch := make(chan ports.OutputFormats, 1)
307 o.formats = ch
308 if err := o.powerOff(); err != nil {
309 t.Fatal(err)
310 }
311 select {
312 case f := <-ch:
313 if f.HDR != nil || len(f.Formats) != 0 {
314 t.Fatalf("power-off report: %+v", f)
315 }
316 default:
317 t.Fatal("no report after power off")
318 }
319}
320
321// A plane without IN_FORMATS (driver without modifiers) scans out its
322// GETPLANE formats, linear.
323func TestPlaneWithoutInFormatsIsLinear(t *testing.T) {
324 k := newMockkms(t)
325 k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1, formats: []uint32{fourccXRGB}}}, nil)
326 k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}}, nil)
327 ps, err := readPlanes(k, 0)
328 if err != nil || len(ps) != 1 {
329 t.Fatalf("%v %v", ps, err)
330 }
331 o := &Output{primary: ps[0]}
332 if f, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB}); !ok || f != fourccXRGB {
333 t.Fatalf("linear argb: %#x %v", f, ok)
334 }
335 if _, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB, Modifier: 0x0200000000000001}); ok {
336 t.Fatal("tiled accepted without IN_FORMATS")
337 }
338}
339
340// A flip shows only the windows its scene draws: presentation feedback of
341// a window on another workspace, hidden or scrolled off the output is
342// discarded, not presented.
343func TestShownByScene(t *testing.T) {
344 s := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 5, W: 10, H: 10}}, {ID: 2, Hidden: true}, {ID: 3, Rect: ports.Rect{X: 10, W: 10, H: 10}}}, Layers: []ports.SceneLayer{{ID: 5}}}
345 got := (&Output{}).shownBy(s, map[ports.WindowID]uint64{1: 3, 2: 4, 3: 8, 5: 6, 9: 7})
346 if len(got) != 2 || got[1] != 3 || got[5] != 6 {
347 t.Fatalf("shows %v", got)
348 }
349}
350
351// The decision is callable without Run and keeps the zero-allocation
352// composition and cached direct-scanout paths cheap.
353func TestFrameDecisionAllocations(t *testing.T) {
354 o, _, _ := testOutput(t)
355 o.scanout = true
356 s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Fullscreen: true, Rect: ports.Rect{W: 200, H: 100}}}}
357 c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, DMABuf: &ports.DMABuf{ID: 17, Format: fourccXRGB}}
358 surfaces := map[ports.WindowID]ports.SurfaceContent{1: c}
359 o.clientFBs[17] = &clientFB{fbID: 77}
360 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
361 for _, tc := range []struct {
362 name string
363 capture bool
364 wantFB uint32
365 wantReason string
366 }{
367 {"direct", false, 77, ""}, {"capture", true, 0, "capture"},
368 } {
369 t.Run(tc.name, func(t *testing.T) {
370 d := o.decideFrame(s, surfaces, tc.capture)
371 if d.fb != tc.wantFB || o.reason != tc.wantReason {
372 t.Fatalf("decision: fb=%d reason=%q", d.fb, o.reason)
373 }
374 if tc.capture && o.overlayReason != "capture" {
375 t.Fatalf("capture overlay reason %q", o.overlayReason)
376 }
377 if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, tc.capture) }); n != 0 {
378 t.Fatalf("decision: %.1f allocs", n)
379 }
380 })
381 }
382 o.primary.formats = nil
383 d := o.decideFrame(s, surfaces, false)
384 if d.fb != 0 || o.overlayReason != "no_plane" || len(d.composed.Windows) != 1 {
385 t.Fatalf("composition: %+v", d)
386 }
387 if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, false) }); n != 0 {
388 t.Fatalf("composition decision: %.1f allocs", n)
389 }
390
391 // Overlay active: the scene composed without the overlaid window reuses
392 // the output's window slice.
393 t.Run("overlay", func(t *testing.T) {
394 o, _, _ := overlayOutput(t)
395 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
396 s, surfaces := overlayScene()
397 o.clientFBs[9] = &clientFB{fbID: 88}
398 d := o.decideFrame(s, surfaces, false)
399 if d.fb != 0 || d.overlay.fb != 88 || o.overlayReason != "" || len(d.composed.Windows) != 1 || d.composed.Windows[0].ID != 1 {
400 t.Fatalf("overlay decision: %+v reason %q", d, o.overlayReason)
401 }
402 if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, false) }); n != 0 {
403 t.Fatalf("overlay decision: %.1f allocs", n)
404 }
405 // The slice is reused: the previous scene's Windows is overwritten
406 // by the next call, and the input scene is never modified.
407 first := d.composed.Windows
408 d2 := o.decideFrame(s, surfaces, false)
409 if &first[0] != &d2.composed.Windows[0] || len(s.Windows) != 2 || s.Windows[0].ID != 1 || s.Windows[1].ID != 2 {
410 t.Fatal("rest windows not reused or the input scene changed")
411 }
412 })
413}
414
415func TestFrameDecisionOverlayFallback(t *testing.T) {
416 o, k, _ := overlayOutput(t)
417 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
418 s, surfaces := overlayScene()
419 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
420 d := o.decideFrame(s, surfaces, false)
421 if d.fb != 0 || d.overlay.fb != 88 || o.reason != "other_windows" || len(d.composed.Windows) != 1 {
422 t.Fatalf("overlay decision: %+v", d)
423 }
424 d.overlay.close()
425 o.clientFBs[9].overlayFailed = "overlay_refused"
426 d = o.decideFrame(s, surfaces, false)
427 if d.overlay.fb != 0 || o.overlayReason != "overlay_refused" || len(d.composed.Windows) != 2 {
428 t.Fatalf("fallback decision: %+v", d)
429 }
430}