output_test.go RawHistoryFolder

1package drm
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"image"
8	"maps"
9	"os"
10	"strings"
11	"sync"
12	"sync/atomic"
13	"testing"
14	"testing/synctest"
15	"time"
16
17	"github.com/bnema/neferwl/internal/adapters/outputkit/presented"
18	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
19	"github.com/bnema/neferwl/internal/ports"
20	"github.com/bnema/zerowrap"
21	"github.com/stretchr/testify/mock"
22	"golang.org/x/sys/unix"
23)
24
25// Object and property IDs of the test output.
26const (
27	tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52
28	pFB, pCrtcID, pFence, pMode, pActive      = 1, 2, 3, 4, 5
29	pVRR, pConnCrtc                           = 6, 7
30)
31
32var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17}
33
34// commitRec is one recorded atomic commit.
35type commitRec struct {
36	req   atomicReq
37	flags uint32
38	user  uint64
39	at    time.Time // when the commit was made
40}
41
42// testOutput returns an output on a mocked kms that records commits;
43// commit returns the next error of errs (nil when empty).
44func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
45	o, k, commits, _ := testOutputMu(t, errs...)
46	return o, k, commits
47}
48
49// testOutputMu is testOutput with the mutex that guards the recorded
50// commits (Run commits on its goroutine).
51func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
52	return testOutputRules(t, nil, errs...)
53}
54
55// testOutputRules is testOutputMu with an optional driver rule: a non-nil
56// rule sees every commit (under the commit mutex) and may refuse it.
57func testOutputRules(t *testing.T, rule func(*atomicReq, uint32) error, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
58	k := newMockkms(t)
59	var commits []commitRec
60	commitMu := &sync.Mutex{}
61	k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error {
62		commitMu.Lock()
63		defer commitMu.Unlock()
64		c := commitRec{flags: flags, user: user, at: time.Now()}
65		c.req.objs = append([]uint32(nil), r.objs...)
66		for _, ps := range r.props {
67			c.req.props = append(c.req.props, append([]propValue(nil), ps...))
68		}
69		commits = append(commits, c)
70		if rule != nil {
71			if err := rule(r, flags); err != nil {
72				return err
73			}
74		}
75		if len(errs) > 0 {
76			err := errs[0]
77			errs = errs[1:]
78			return err
79		}
80		return nil
81	}).Maybe()
82	primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps}
83	cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64)
84	cur.fbs = [2]uint32{90, 91}
85	o := &Output{k: k, frame: frameLifecycle{serials: &atomic.Uint64{}}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(), renderLog: zerowrap.Default(),
86		crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR,
87		primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true, planeRect: fullPlaneRect(200, 100), seatDisable: make(chan chan struct{})}
88	return o, k, &commits, commitMu
89}
90
91// eventOf is the flip event of commit c.
92func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} }
93
94func objCount(c commitRec) int { return len(c.req.objs) }
95
96func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) {
97	o, _, commits := testOutput(t)
98	o.cursor.image = true
99	o.cursor.Move(12, 34)
100	fence, w, err := os.Pipe()
101	if err != nil {
102		t.Fatal(err)
103	}
104	defer fence.Close()
105	defer w.Close()
106	if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
107		t.Fatal(err)
108	}
109	c := (*commits)[0]
110	if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.frame.pendingSerial {
111		t.Fatalf("flags %#x user %d", c.flags, c.user)
112	}
113	if v, _ := c.req.value(tPrimary, pFB); v != 70 {
114		t.Fatalf("primary fb %d", v)
115	}
116	if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) {
117		t.Fatalf("fence %d %v", v, ok)
118	}
119	if v, _ := c.req.value(tCursor, 14); v != 12 {
120		t.Fatalf("cursor x %d", v)
121	}
122	if v, _ := c.req.value(tCrtc, pVRR); v != 1 {
123		t.Fatalf("vrr %d", v)
124	}
125	if !o.frame.pendingCommit() || !o.vrrOn || o.cursor.applied.x != 12 {
126		t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.frame.pendingCommit(), o.vrrOn, o.cursor.applied)
127	}
128}
129
130func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) {
131	o, _, commits := testOutput(t)
132	fence, w, _ := os.Pipe()
133	defer fence.Close()
134	defer w.Close()
135	if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
136		t.Fatal(err)
137	}
138	c := (*commits)[0]
139	if c.flags&flipAsyncFlag == 0 {
140		t.Fatal("not async")
141	}
142	if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 {
143		t.Fatalf("async commit %+v", c.req)
144	}
145}
146
147func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) {
148	o, _, commits := testOutput(t)
149	o.cursor.image = true
150	o.cursor.Move(5, 5)
151	if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil {
152		t.Fatal(err)
153	}
154	if (*commits)[0].flags&flipAsyncFlag != 0 {
155		t.Fatal("async commit moved the cursor")
156	}
157}
158
159func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) {
160	o, _, commits := testOutput(t)
161	o.asyncFence = false
162	fence, w, _ := os.Pipe()
163	defer fence.Close()
164	defer w.Close()
165	if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
166		t.Fatal(err)
167	}
168	if (*commits)[0].flags&flipAsyncFlag != 0 {
169		t.Fatal("async commit with a fence the driver refuses")
170	}
171}
172
173func TestScanoutAsyncRefusedRetriesSync(t *testing.T) {
174	o, _, commits := testOutput(t, unix.EINVAL)
175	o.shown, o.vrrOn = 1, true
176	buf := &ports.DMABuf{ID: 9}
177	o.clientFBs[9] = &clientFB{fbID: 80}
178	ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{})
179	if !ok || err != nil {
180		t.Fatalf("ok=%v err=%v", ok, err)
181	}
182	if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 {
183		t.Fatalf("commits %+v", *commits)
184	}
185	if !o.clientFBs[9].noAsync || o.queued != 9 {
186		t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued)
187	}
188}
189
190func TestCursorCommitWaitsForEvent(t *testing.T) {
191	o, _, commits := testOutput(t)
192	o.cursor.image = true
193	o.cursor.Move(1, 1)
194	if err := o.commitState(false); err != nil {
195		t.Fatal(err)
196	}
197	c := (*commits)[0]
198	if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.frame.pendingCommit() {
199		t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.frame.pendingCommit())
200	}
201	// Its event is not a frame: nothing is reported.
202	seen := map[ports.WindowID]uint64{}
203	o.completed(eventOf(c), seen)
204	if o.frame.pendingCommit() || o.reports.Len() != 0 || o.flips != 0 {
205		t.Fatalf("pending=%v unsent=%d flips=%d", o.frame.pendingCommit(), o.reports.Len(), o.flips)
206	}
207	// Nothing changed: no commit.
208	if err := o.commitState(false); err != nil || len(*commits) != 1 {
209		t.Fatalf("idle commit: %v, %d commits", err, len(*commits))
210	}
211}
212
213// Under VRR with a game shown, a cursor move rides on the next frame: a
214// cursor-only commit would hold the panel for its slowest refresh. A game
215// that stops drawing still gets the cursor after cursorMinInterval.
216func TestVRRGameCursorWaitsForFrame(t *testing.T) {
217	o, _, commits := testOutput(t)
218	o.cursor.image = true
219	o.vrrOn, o.vrrGame = true, true
220	o.lastFrame = time.Now()
221	o.cursor.Move(5, 5)
222	if err := o.commitState(o.stateVRR()); err != nil {
223		t.Fatal(err)
224	}
225	if len(*commits) != 0 || !o.cursorHeld || o.frame.pendingCommit() {
226		t.Fatalf("cursor committed alone: %d commits held=%v", len(*commits), o.cursorHeld)
227	}
228	// The next frame carries the cursor.
229	if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
230		t.Fatal(err)
231	}
232	if v, _ := (*commits)[0].req.value(tCursor, 14); v != 5 || o.cursorHeld {
233		t.Fatalf("frame cursor x %d held=%v", v, o.cursorHeld)
234	}
235	o.completed(eventOf((*commits)[0]), map[ports.WindowID]uint64{})
236	// The game stalls: the cursor commits alone after the interval.
237	o.lastFrame = time.Now().Add(-cursorMinInterval)
238	o.cursor.Move(8, 8)
239	if err := o.commitState(o.stateVRR()); err != nil {
240		t.Fatal(err)
241	}
242	if len(*commits) != 2 || o.cursorHeld {
243		t.Fatalf("stalled game: %d commits held=%v", len(*commits), o.cursorHeld)
244	}
245	if v, _ := (*commits)[1].req.value(tCursor, 14); v != 8 {
246		t.Fatalf("cursor x %d", v)
247	}
248}
249
250// A held move reversed before the next frame holds nothing: the cursor
251// timer is not armed again.
252func TestHeldCursorMovedBackClearsHold(t *testing.T) {
253	o, _, commits := testOutput(t)
254	o.cursor.image = true
255	o.vrrOn, o.vrrGame, o.lastFrame = true, true, time.Now()
256	o.cursor.Move(0, 0)
257	o.cursor.applied = o.cursor.desired() // on screen at (0, 0)
258	o.cursor.Move(5, 5)
259	if err := o.commitState(o.stateVRR()); err != nil || !o.cursorHeld {
260		t.Fatalf("err %v held=%v", err, o.cursorHeld)
261	}
262	o.cursor.Move(0, 0)
263	if err := o.commitState(o.stateVRR()); err != nil || o.cursorHeld || len(*commits) != 0 {
264		t.Fatalf("err %v held=%v commits %d", err, o.cursorHeld, len(*commits))
265	}
266}
267
268// Without a game, the cursor keeps its own commits.
269func TestCursorCommitsAloneWithoutGame(t *testing.T) {
270	o, _, commits := testOutput(t)
271	o.cursor.image = true
272	o.vrrOn, o.lastFrame = true, time.Now()
273	o.composedSince = time.Now()
274	o.cursor.Move(5, 5)
275	if err := o.commitState(o.stateVRR()); err != nil || len(*commits) != 1 || o.cursorHeld {
276		t.Fatalf("err %v, %d commits held=%v", err, len(*commits), o.cursorHeld)
277	}
278}
279
280func TestBusyCommitStaysPending(t *testing.T) {
281	o, _, _ := testOutput(t, unix.EBUSY)
282	err := o.commitFrame(70, nil, false, false, pendingFrame{})
283	enabled := true
284	if !o.commitFailed(err, &enabled) || !o.frame.pendingCommit() || !enabled {
285		t.Fatalf("EBUSY: pending=%v enabled=%v", o.frame.pendingCommit(), enabled)
286	}
287}
288
289func TestFlipReportsTimestampAndShownFrame(t *testing.T) {
290	o, _, commits := testOutput(t)
291	o.vrrOn = false
292	o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0
293	if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil {
294		t.Fatal(err)
295	}
296	o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4})
297	if o.reports.Len() != 1 {
298		t.Fatalf("reports %d", o.reports.Len())
299	}
300	f := o.reports.Unsent()[0].Flip
301	if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock {
302		t.Fatalf("flip %+v", f)
303	}
304}
305
306// Unchanged reports share an immutable snapshot; a later content update
307// must not change an earlier report, even after it has been sent.
308func TestReportSeenAllocations(t *testing.T) {
309	o, _, _ := testOutput(t)
310	seen := map[ports.WindowID]uint64{1: 1, 2: 2}
311	o.report(nil, seen)
312	// Keep the queue bounded without measuring the queue's growth.
313	if allocs := testing.AllocsPerRun(100, func() { o.report(nil, seen) }); allocs != 0 {
314		t.Errorf("unchanged report: %.1f allocs, want 0", allocs)
315	} else {
316		t.Logf("unchanged report: %.1f allocs", allocs)
317	}
318	previous := o.reports.Unsent()[0].Seen
319	seen[1] = 3
320	o.report(nil, seen)
321	if previous[1] != 1 || o.reports.Unsent()[0].Seen[1] != 3 {
322		t.Fatalf("snapshot mutated: old %v new %v", previous, o.reports.Unsent()[0].Seen)
323	}
324}
325
326func TestShownBySnapshotAllocations(t *testing.T) {
327	o := &Output{}
328	s := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
329	seen := map[ports.WindowID]uint64{1: 1, 2: 2}
330	previous := o.shownBy(s, seen)
331	if allocs := testing.AllocsPerRun(100, func() { o.shownBy(s, seen) }); allocs != 0 {
332		t.Errorf("unchanged shows: %.1f allocs, want 0", allocs)
333	}
334	seen[1] = 3
335	current := o.shownBy(s, seen)
336	if previous[1] != 1 || current[1] != 3 {
337		t.Fatalf("snapshot mutated: old %v new %v", previous, current)
338	}
339	direct := o.directShownBy(1, 3)
340	if allocs := testing.AllocsPerRun(100, func() { o.directShownBy(1, 3) }); allocs != 0 {
341		t.Errorf("unchanged direct shows: %.1f allocs, want 0", allocs)
342	}
343	o.directShownBy(1, 4)
344	if direct[1] != 3 {
345		t.Fatalf("direct snapshot mutated: %v", direct)
346	}
347}
348
349func TestReportsKeepEveryFlip(t *testing.T) {
350	o, _, _ := testOutput(t)
351	seen := map[ports.WindowID]uint64{1: 1}
352	o.report(&ports.FlipInfo{Seq: 1}, seen)
353	o.report(nil, seen)
354	o.report(&ports.FlipInfo{Seq: 2}, seen)
355	o.report(nil, map[ports.WindowID]uint64{1: 3})
356	if o.reports.Len() != 2 || o.reports.Unsent()[0].Flip.Seq != 1 || o.reports.Unsent()[1].Flip.Seq != 2 || o.reports.Unsent()[1].Seen[1] != 3 {
357		t.Fatalf("unsent %+v", o.reports.Unsent())
358	}
359	for i := range presented.MaxUnsent + 3 {
360		o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen)
361	}
362	if o.reports.Len() != presented.MaxUnsent || o.reports.Unsent()[0].Flip.Merged == 0 {
363		t.Fatalf("cap: %d reports, first merged %d", o.reports.Len(), o.reports.Unsent()[0].Flip.Merged)
364	}
365}
366
367func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) {
368	o, k, commits := testOutput(t)
369	stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps}
370	o.stray = []*plane{stray}
371	k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
372	k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
373	if err := o.modeset(); err != nil {
374		t.Fatal(err)
375	}
376	c := (*commits)[0]
377	if c.flags != atomicAllowModes {
378		t.Fatalf("flags %#x", c.flags)
379	}
380	if v, _ := c.req.value(tCrtc, pMode); v != 99 {
381		t.Fatalf("mode blob %d", v)
382	}
383	for _, p := range []uint32{tCursor, tOverlay} {
384		if v, ok := c.req.value(p, pFB); !ok || v != 0 {
385			t.Fatalf("plane %d fb %d %v", p, v, ok)
386		}
387	}
388	if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
389		t.Fatalf("connector crtc %d", v)
390	}
391	// The next modeset replaces the blob.
392	k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once()
393	k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once()
394	k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
395	if err := o.modeset(); err != nil {
396		t.Fatal(err)
397	}
398	if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok {
399		t.Fatal("plane on another CRTC turned off")
400	}
401}
402
403func TestAsyncProbeAfterModeset(t *testing.T) {
404	o, _, commits := testOutput(t, unix.EINVAL)
405	o.asyncFence = true
406	r := portsmocks.NewMockRenderer(t)
407	fence, w, _ := os.Pipe()
408	defer w.Close()
409	r.EXPECT().UseTarget(mock.Anything).Return().Once()
410	r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once()
411	o.probeAsync(r)
412	c := (*commits)[0]
413	if c.flags != atomicTestOnly|flipAsyncFlag {
414		t.Fatalf("flags %#x", c.flags)
415	}
416	if _, ok := c.req.value(tPrimary, pFence); !ok {
417		t.Fatal("probe without a fence")
418	}
419	if o.asyncFence {
420		t.Fatal("refused probe kept asyncFence")
421	}
422	if _, err := fence.Stat(); err == nil {
423		t.Fatal("probe fence not closed")
424	}
425	o.probeAsync(r) // once only
426}
427
428func TestVRRToggleOnlyInsideFrames(t *testing.T) {
429	o, _, commits := testOutput(t)
430	o.vrrOn = true
431	o.composedSince = time.Now().Add(-time.Second)
432	if o.wantVRR(false) {
433		t.Fatal("VRR kept after vrrHold of composition")
434	}
435	o.composedSince = time.Now()
436	if !o.wantVRR(false) {
437		t.Fatal("VRR dropped before vrrHold")
438	}
439	if err := o.commitState(false); err != nil {
440		t.Fatal(err)
441	}
442	if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 {
443		t.Fatalf("vrr off commit %+v", (*commits)[0])
444	}
445}
446
447// A composed fullscreen frame keeps VRR and restarts the hold; state
448// commits (cursor, vrrOff timer) keep it too until the fullscreen ends.
449func TestVRRKeptWhileFullscreenComposed(t *testing.T) {
450	o, _, _ := testOutput(t)
451	o.vrrOn = true
452	o.composedSince = time.Now().Add(-time.Second)
453	if !o.wantVRR(true) || !o.composedSince.IsZero() {
454		t.Fatal("composed fullscreen dropped VRR")
455	}
456	// Cursor-only state commit between frames.
457	if !o.stateVRR() {
458		t.Fatal("state commit dropped VRR during fullscreen")
459	}
460	// Fullscreen ends, a tiled window stays on the overlay plane: the
461	// hold starts, then VRR goes off.
462	o.shown = 9
463	if !o.wantVRR(false) || o.composedSince.IsZero() {
464		t.Fatal("hold not started on exit")
465	}
466	o.composedSince = time.Now().Add(-time.Second)
467	if o.stateVRR() {
468		t.Fatal("VRR kept after the hold (overlay buffer taken for a game)")
469	}
470}
471
472// A fullscreen game on a workspace that is left keeps VRR only for the
473// hold: its hidden window no longer counts as a game frame, and its
474// commits do not draw frames.
475func TestVRRHiddenGameOnlyHolds(t *testing.T) {
476	o, _, _ := testOutput(t)
477	game := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
478	shown := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game}}
479	o.vrrOn = true
480	if !o.wantVRR(fullscreenShown(&shown)) {
481		t.Fatal("VRR off while the game is shown")
482	}
483	game.Hidden, game.Rect = true, ports.Rect{}
484	left := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game, {ID: 2, Rect: ports.Rect{W: 100, H: 50}}}}
485	if fullscreenShown(&left) || left.Shows(1) {
486		t.Fatal("hidden game still drives frames")
487	}
488	if !o.wantVRR(fullscreenShown(&left)) {
489		t.Fatal("VRR dropped before the hold")
490	}
491	o.composedSince = time.Now().Add(-vrrHold - time.Millisecond)
492	if o.stateVRR() {
493		t.Fatal("VRR kept after the hold for a hidden game")
494	}
495}
496
497// A VRR refusal on a composed frame with an overlay retries with the
498// overlay: the composed image left its window out.
499func TestVRRRefusedKeepsOverlay(t *testing.T) {
500	o, k, commits := overlayOutput(t, unix.EINVAL)
501	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
502	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
503	s, c := overlayScene()
504	ov, _ := o.overlayFrame(s, c)
505	if ov.fb == 0 {
506		t.Fatal("no overlay")
507	}
508	if err := o.commitWith(70, nil, false, true, pendingFrame{queued: ov.buf}, ov); err != nil {
509		t.Fatal(err)
510	}
511	if len(*commits) != 2 || o.vrrProp != 0 {
512		t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
513	}
514	if v, _ := (*commits)[1].req.value(tOverlay, pFB); v != 88 || o.overlayOn != ov.buf {
515		t.Fatalf("retry overlay fb %d, overlayOn %d", v, o.overlayOn)
516	}
517}
518
519func TestReadPlanes(t *testing.T) {
520	k := newMockkms(t)
521	k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil)
522	k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil)
523	k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil)
524	k.EXPECT().getBlob(uint32(5)).Return(nil, nil)
525	ps, err := readPlanes(k, 0)
526	if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor {
527		t.Fatalf("planes %+v %v", ps, err)
528	}
529}
530
531func TestVRRRefusedIsDisabled(t *testing.T) {
532	o, _, commits := testOutput(t, unix.EINVAL)
533	if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
534		t.Fatal(err)
535	}
536	if len(*commits) != 2 || o.vrrProp != 0 || !o.frame.pendingCommit() {
537		t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
538	}
539	if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 {
540		t.Fatal("retry kept VRR on")
541	}
542}
543
544func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
545	o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
546	o.clientFBs[9] = &clientFB{fbID: 80}
547	ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
548	if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" {
549		t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed)
550	}
551}
552
553// Run: while a frame is in flight its buffers may still be read by the
554// GPU, so a newer content is reported only after the flip event; every
555// frame fence is closed.
556func TestRunReportsSeenAfterFlip(t *testing.T) { synctest.Test(t, runReportsSeenAfterFlip) }
557
558func runReportsSeenAfterFlip(t *testing.T) {
559	o, k, commits, commitMu := testOutputMu(t)
560	o.cursor, o.tearing = nil, false
561	flips := make(chan flipEvent, 1)
562	o.flipped = flips
563	r := portsmocks.NewMockRenderer(t)
564	var mu sync.Mutex
565	var fences []*os.File
566	pipeBuf := func() ports.DMABuf {
567		f, w, _ := os.Pipe()
568		w.Close()
569		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
570	}
571	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
572	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
573	r.EXPECT().UseTarget(mock.Anything).Return()
574	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
575		f, w, _ := os.Pipe()
576		_, _ = w.Write([]byte{1}) // readable: signalled
577		w.Close()
578		mu.Lock()
579		fences = append(fences, f)
580		mu.Unlock()
581		return f, nil
582	})
583	r.EXPECT().Close().Return().Once()
584	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
585	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
586	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
587	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
588	frameCommits := func() int {
589		commitMu.Lock()
590		defer commitMu.Unlock()
591		n := 0
592		for _, c := range *commits {
593			if c.user&3 == userFrame {
594				n++
595			}
596		}
597		return n
598	}
599	scenes := make(chan ports.Scene, 1)
600	contents := make(chan ports.SurfaceContent, 1)
601	presented := make(chan ports.OutputPresented, 8)
602	ctx, cancel := context.WithCancel(context.Background())
603	done := make(chan error, 1)
604	go func() {
605		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented, nil, nil)
606	}()
607	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
608	waitFor(t, func() bool { return frameCommits() == 1 })
609	// Frame 1 is in flight: content 3 replaces what it may be reading.
610	contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}
611	synctest.Wait()
612	select {
613	case p := <-presented:
614		t.Fatalf("report while a frame is in flight: %+v", p)
615	default:
616	}
617	commitMu.Lock()
618	last := (*commits)[len(*commits)-1]
619	commitMu.Unlock()
620	flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
621	select {
622	case p := <-presented:
623		if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 {
624			t.Fatalf("report %+v", p)
625		}
626	case <-time.After(2 * time.Second):
627		t.Fatal("no report after the flip")
628	}
629	cancel()
630	if err := <-done; err != nil {
631		t.Fatal(err)
632	}
633	mu.Lock()
634	defer mu.Unlock()
635	if len(fences) < 3 {
636		t.Fatalf("%d renders", len(fences))
637	}
638	for _, f := range fences {
639		if _, err := f.Stat(); err == nil {
640			t.Fatal("frame fence left open")
641		}
642	}
643}
644
645// Run: a fullscreen window composed under an overlay layer keeps VRR on in
646// the frame commit; a tiled scene turns it off only after vrrHold.
647func TestRunComposedFullscreenKeepsVRR(t *testing.T) {
648	o, k, commits, commitMu := testOutputMu(t)
649	o.cursor, o.tearing = nil, false
650	flips := make(chan flipEvent, 1)
651	o.flipped = flips
652	r := portsmocks.NewMockRenderer(t)
653	pipeBuf := func() ports.DMABuf {
654		f, w, _ := os.Pipe()
655		w.Close()
656		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
657	}
658	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
659	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
660	r.EXPECT().UseTarget(mock.Anything).Return()
661	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
662	r.EXPECT().Close().Return().Once()
663	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
664	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
665	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
666	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
667	lastFrame := func() (commitRec, int) {
668		commitMu.Lock()
669		defer commitMu.Unlock()
670		n, last := 0, commitRec{}
671		for _, c := range *commits {
672			if c.user&3 == userFrame {
673				n, last = n+1, c
674			}
675		}
676		return last, n
677	}
678	scenes := make(chan ports.Scene, 1)
679	ctx, cancel := context.WithCancel(context.Background())
680	done := make(chan error, 1)
681	go func() {
682		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil)
683	}()
684	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100,
685		Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}},
686		Layers:  []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}}}
687	waitFor(t, func() bool { _, n := lastFrame(); return n == 1 })
688	last, _ := lastFrame()
689	if v, ok := last.req.value(tCrtc, pVRR); !ok || v != 1 {
690		t.Fatalf("composed fullscreen frame VRR %d %v", v, ok)
691	}
692	flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
693	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 100, H: 100}}}}
694	waitFor(t, func() bool { _, n := lastFrame(); return n == 2 })
695	last, _ = lastFrame()
696	if v, _ := last.req.value(tCrtc, pVRR); v != 1 {
697		t.Fatal("VRR dropped before vrrHold")
698	}
699	flips <- flipEvent{crtc: tCrtc, user: last.user, when: 2 * time.Second, seq: 2}
700	// The vrrOff timer turns VRR off with a state commit after the hold.
701	waitFor(t, func() bool {
702		commitMu.Lock()
703		defer commitMu.Unlock()
704		c := (*commits)[len(*commits)-1]
705		v, ok := c.req.value(tCrtc, pVRR)
706		return c.user&3 == userState && ok && v == 0
707	})
708	cancel()
709	if err := <-done; err != nil {
710		t.Fatal(err)
711	}
712}
713
714func waitFor(t *testing.T, cond func() bool) {
715	t.Helper()
716	deadline := time.Now().Add(2 * time.Second)
717	for !cond() {
718		if time.Now().After(deadline) {
719			t.Fatal("timed out")
720		}
721		time.Sleep(2 * time.Millisecond)
722	}
723}
724
725// The stats tick lets the renderer free what the output no longer draws,
726// even while nothing renders; a failed trim stops the output.
727func TestRunTrimsRendererOnStatsTick(t *testing.T) {
728	o, k, _ := testOutput(t)
729	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
730	ticks := make(chan time.Time)
731	clk := portsmocks.NewMockClock(t)
732	tk := portsmocks.NewMockTicker(t)
733	clk.EXPECT().NewTicker(10 * time.Second).Return(tk).Once()
734	tk.EXPECT().C().Return(ticks)
735	tk.EXPECT().Stop().Return().Once()
736	now := time.Unix(1000, 0)
737	clk.EXPECT().Now().Return(now)
738	o.clock = clk
739	r := portsmocks.NewMockRenderer(t)
740	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
741	r.EXPECT().Close().Return().Once()
742	r.EXPECT().TakeRedrawn().Return(1234).Once()
743	var statsLog bytes.Buffer // written by Run's goroutine, read after it ends
744	o.log = zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog})
745	o.flipStats = flipStats{missedVblanks: 3, maxInterval: 50 * time.Millisecond, maxFlipToRead: 25 * time.Millisecond, lateFences: 2}
746	trimmed := make(chan struct{}, 2)
747	failure := os.ErrInvalid
748	calls := 0 // Run's goroutine only
749	r.EXPECT().Trim(now).RunAndReturn(func(time.Time) error {
750		trimmed <- struct{}{}
751		if calls++; calls == 2 {
752			return failure
753		}
754		return nil
755	}).Twice()
756	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
757	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
758	// Switched away from the start: no target, no frame.
759	active := make(chan bool, 1)
760	active <- false
761	done := make(chan error, 1)
762	go func() {
763		done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil)
764	}()
765	ticks <- now
766	<-trimmed
767	ticks <- now
768	if err := <-done; !errors.Is(err, failure) {
769		t.Fatalf("run: %v", err)
770	}
771	// Reported once, then reset. late_fences needs flip tracing.
772	for _, want := range []string{`"message":"stats"`, `"missed_vblanks":3`, `"max_flip_interval_ms":50`, `"max_flip_to_read_ms":25`, `"redrawn_pixels":1234`} {
773		if !strings.Contains(statsLog.String(), want) {
774			t.Fatalf("stats entry lacks %s: %s", want, statsLog.String())
775		}
776	}
777	if strings.Contains(statsLog.String(), "late_fences") || o.flipStats != (flipStats{}) {
778		t.Fatalf("late fences untraced or stats not reset: %s %+v", statsLog.String(), o.flipStats)
779	}
780}
781
782// A render failure must complete captures queued before the first frame.
783func TestCaptureRenderFailureClosesPendingRequest(t *testing.T) {
784	o, k, _ := testOutput(t)
785	o.cursor, o.tearing = nil, false
786	r := portsmocks.NewMockRenderer(t)
787	boom := errors.New("render failed")
788	r.EXPECT().SetHDR(float64(0)).Return().Once()
789	r.EXPECT().UseTarget(mock.Anything).Return()
790	makeBuf := func() ports.DMABuf {
791		f, err := os.CreateTemp(t.TempDir(), "target")
792		if err != nil {
793			t.Fatal(err)
794		}
795		t.Cleanup(func() { _ = f.Close() })
796		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
797	}
798	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
799	r.EXPECT().Render(ports.Scene{Background: "#000000"}, mock.Anything).Return(nil, nil).Twice()
800	r.EXPECT().Render(mock.MatchedBy(func(s ports.Scene) bool { return s.Background != "#000000" }), mock.Anything).Return(nil, boom).Once()
801	r.EXPECT().Close().Return().Once()
802	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
803	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
804	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
805	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
806	ctx, cancel := context.WithCancel(context.Background())
807	defer cancel()
808	scenes := make(chan ports.Scene, 1)
809	requests := make(chan ports.CaptureRequest)
810	replies := make(chan ports.CaptureDone, 1)
811	f, err := os.CreateTemp(t.TempDir(), "capture")
812	if err != nil {
813		t.Fatal(err)
814	}
815	done := make(chan error, 1)
816	go func() {
817		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, requests, replies)
818	}()
819	// The unbuffered handoff proves Run owns the request before any scene.
820	select {
821	case requests <- ports.CaptureRequest{ID: 9, Dst: ports.SHMBuffer{File: f}}:
822	case <-time.After(2 * time.Second):
823		t.Fatal("output did not accept capture")
824	}
825	scenes <- ports.Scene{Seq: 1, Scale: 1}
826	select {
827	case err := <-done:
828		if !errors.Is(err, boom) {
829			t.Fatalf("run error %v", err)
830		}
831	case <-time.After(2 * time.Second):
832		t.Fatal("output did not stop after render failure")
833	}
834	select {
835	case result := <-replies:
836		if result.ID != 9 || result.Err == nil {
837			t.Fatalf("capture result %+v", result)
838		}
839	default:
840		t.Fatal("render failure lost capture completion")
841	}
842	if _, err := f.Stat(); err == nil {
843		t.Fatal("render failure leaked capture descriptor")
844	}
845}
846
847// A request must render even when the output already has an unchanged scene.
848func TestCaptureForcesDRMComposition(t *testing.T) {
849	o, k, commits, commitMu := testOutputMu(t)
850	o.cursor, o.tearing = nil, false
851	flips := make(chan flipEvent, 2)
852	o.flipped = flips
853	r := &portsmocks.MockRenderer{}
854	makeBuf := func() ports.DMABuf {
855		f, w, _ := os.Pipe()
856		w.Close()
857		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
858	}
859	r.EXPECT().SetHDR(float64(0)).Return().Maybe() // SDR output
860	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
861	r.EXPECT().UseTarget(mock.Anything).Return()
862	rendered := make(chan ports.Scene, 3)
863	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
864		rendered <- s
865		return nil, nil
866	})
867	frame := portsmocks.NewMockCaptureFrame(t)
868	frame.EXPECT().Done().Return(nil)
869	frame.EXPECT().Read(image.Rect(0, 0, 2, 2), mock.MatchedBy(func(p []byte) bool { return len(p) >= 16 }), 8).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error { copy(dst, []byte{3, 4, 5, 255}); return nil }).Once()
870	r.EXPECT().BeginCapture().Return(frame, nil).Once()
871	r.EXPECT().EndCapture(frame).Return().Once()
872	r.EXPECT().Close().Return().Once()
873	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
874	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
875	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
876	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
877	scenes := make(chan ports.Scene, 1)
878	requests := make(chan ports.CaptureRequest, 1)
879	replies := make(chan ports.CaptureDone, 1)
880	ctx, cancel := context.WithCancel(context.Background())
881	done := make(chan error, 1)
882	go func() {
883		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, make(chan ports.OutputPresented, 8), requests, replies)
884	}()
885	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"}
886	select {
887	case <-rendered:
888	case <-time.After(2 * time.Second):
889		t.Fatal("no initial render")
890	}
891	waitFor(t, func() bool { commitMu.Lock(); defer commitMu.Unlock(); return len(*commits) > 0 })
892	commitMu.Lock()
893	last := (*commits)[len(*commits)-1]
894	commitMu.Unlock()
895	flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
896	f, err := os.CreateTemp(t.TempDir(), "capture")
897	if err != nil {
898		t.Fatal(err)
899	}
900	if err := f.Truncate(16); err != nil {
901		t.Fatal(err)
902	}
903	requests <- ports.CaptureRequest{ID: 4, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Dst: ports.SHMBuffer{File: f}}
904	select {
905	case result := <-replies:
906		if result.Err != nil {
907			t.Fatal(result.Err)
908		}
909	case <-time.After(2 * time.Second):
910		t.Fatal("capture not replied")
911	}
912	select {
913	case <-rendered:
914	case <-time.After(2 * time.Second):
915		t.Fatal("capture did not render")
916	}
917	frame.Calls = nil // Testify must not inspect unmapped slice during expectation cleanup.
918	if _, err := f.Stat(); err == nil {
919		t.Fatal("fd not closed")
920	}
921	cancel()
922	if err := <-done; err != nil {
923		t.Fatal(err)
924	}
925}
926
927func TestRunPanicReportsReadinessError(t *testing.T) {
928	o, _, _ := testOutput(t)
929	o.ready = make(chan error, 1)
930	defer func() {
931		if recover() == nil {
932			t.Fatal("panic not propagated")
933		}
934		select {
935		case err := <-o.Ready():
936			if err == nil {
937				t.Fatal("panic reported as ready")
938			}
939		default:
940			t.Fatal("missing readiness")
941		}
942	}()
943	o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil)
944}
945
946// stampRun starts Run on an output with unbuffered scene and content
947// channels: a send completes once Run took the message, and the next send
948// once it went round its loop again, which is the sync point for what the
949// loop did with the first.
950type stampRun struct {
951	o           *Output
952	scenes      chan ports.Scene
953	contents    chan ports.SurfaceContent
954	flips       chan flipEvent
955	wake        chan ports.SecurityState
956	state       *atomic.Uint64
957	frameCommit func() int
958	lastCommit  func() commitRec
959	stop        func()
960}
961
962func startStampRun(t *testing.T) *stampRun {
963	o, k, commits, commitMu := testOutputMu(t)
964	o.cursor, o.tearing = nil, false
965	sr := &stampRun{o: o, scenes: make(chan ports.Scene), contents: make(chan ports.SurfaceContent), flips: make(chan flipEvent, 1), wake: make(chan ports.SecurityState, 1), state: &atomic.Uint64{}}
966	o.flipped = sr.flips
967	securityGate(t, o, sr.state)
968	o.SecurityChanges = sr.wake
969	r := portsmocks.NewMockRenderer(t)
970	pipeBuf := func() ports.DMABuf {
971		f, w, _ := os.Pipe()
972		w.Close()
973		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
974	}
975	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
976	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
977	r.EXPECT().UseTarget(mock.Anything).Return()
978	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
979	r.EXPECT().Close().Return().Once()
980	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
981	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
982	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
983	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
984	sr.frameCommit = func() int {
985		commitMu.Lock()
986		defer commitMu.Unlock()
987		n := 0
988		for _, c := range *commits {
989			if c.user&3 == userFrame {
990				n++
991			}
992		}
993		return n
994	}
995	sr.lastCommit = func() commitRec {
996		commitMu.Lock()
997		defer commitMu.Unlock()
998		return (*commits)[len(*commits)-1]
999	}
1000	ctx, cancel := context.WithCancel(context.Background())
1001	done := make(chan error, 1)
1002	go func() {
1003		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents, nil, make(chan ports.OutputPresented, 8), nil, nil)
1004	}()
1005	// stop ends Run: its state is ours to read afterwards.
1006	sr.stop = func() {
1007		cancel()
1008		if err := <-done; err != nil {
1009			t.Fatal(err)
1010		}
1011	}
1012	return sr
1013}
1014
1015// Run: a frame wanted while the previous one is in flight is stamped when
1016// it becomes dirty, and the commit that follows takes the stamp.
1017func TestRunStampsWhenFrameWasWanted(t *testing.T) {
1018	sr := startStampRun(t)
1019	scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1020	sr.scenes <- scene
1021	waitFor(t, func() bool { return sr.frameCommit() == 1 })
1022	// Frame 1 is in flight: a new scene arrives and waits.
1023	last := sr.lastCommit()
1024	scene.Seq = 2
1025	before := monotonic()
1026	sr.scenes <- scene
1027	sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // Run went round its loop
1028	flipSent := monotonic()
1029	sr.flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
1030	waitFor(t, func() bool { return sr.frameCommit() == 2 })
1031	sr.stop()
1032	if sr.o.wantedAt != 0 {
1033		t.Fatalf("stamp left after the commit: %s", sr.o.wantedAt)
1034	}
1035	f := sr.o.frame.pendingFrame
1036	if !f.frame || f.wantedAt < before || f.wantedAt >= flipSent {
1037		t.Fatalf("second frame (%v) wanted at %s, scene sent at %s, flip sent at %s", f.frame, f.wantedAt, before, flipSent)
1038	}
1039}
1040
1041// A change of security state drops the scene, and with it the stamp of
1042// the frame it wanted.
1043func TestRunSecurityResetClearsWantedAt(t *testing.T) {
1044	sr := startStampRun(t)
1045	scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1046	sr.scenes <- scene
1047	waitFor(t, func() bool { return sr.frameCommit() == 1 })
1048	scene.Seq = 2
1049	sr.scenes <- scene
1050	sr.contents <- ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}} // stamped by now
1051	// A new security generation, not protected.
1052	sr.state.Store(2)
1053	sr.wake <- ports.SecurityState{}
1054	sr.contents <- ports.SurfaceContent{ID: 1, Seq: 2, SHM: &ports.SHMBuffer{}} // Run went round and reset
1055	sr.stop()
1056	if sr.o.wantedAt != 0 {
1057		t.Fatalf("stamp kept across the security reset: %s", sr.o.wantedAt)
1058	}
1059}
1060
1061// A scene whose Seq the output already holds draws nothing new: no frame,
1062// until a scene with a new Seq arrives.
1063func TestOutputSameSeqRendersOnce(t *testing.T) {
1064	sr := startStampRun(t)
1065	scene := ports.Scene{Seq: 5, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1066	sr.scenes <- scene
1067	waitFor(t, func() bool { return sr.frameCommit() == 1 })
1068	sr.flips <- flipEvent{crtc: tCrtc, user: sr.lastCommit().user, when: time.Second, seq: 1}
1069	sr.scenes <- scene
1070	// Unshown content only sends Run round its loop: the flip and the held
1071	// scene are handled by the time the second one is taken.
1072	sr.contents <- ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}}
1073	sr.contents <- ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}}
1074	if n := sr.frameCommit(); n != 1 {
1075		t.Fatalf("%d frames after a scene with the held Seq", n)
1076	}
1077	scene.Seq = 6
1078	sr.scenes <- scene
1079	waitFor(t, func() bool { return sr.frameCommit() == 2 })
1080	sr.stop()
1081}
1082
1083// Run: the empty content of a window that unmapped while the scene still
1084// draws it (its close animation) keeps the last content for the renderer,
1085// without its Acquire fence; a scene that no longer lists the window drops
1086// it.
1087func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) {
1088	o, k, _ := testOutput(t)
1089	o.cursor, o.tearing = nil, false
1090	r := portsmocks.NewMockRenderer(t)
1091	pipeBuf := func() ports.DMABuf {
1092		f, w, _ := os.Pipe()
1093		w.Close()
1094		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
1095	}
1096	var mu sync.Mutex
1097	var last map[ports.WindowID]ports.SurfaceContent
1098	var lastSeq uint64
1099	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
1100	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
1101	r.EXPECT().UseTarget(mock.Anything).Return()
1102	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
1103		mu.Lock()
1104		last, lastSeq = maps.Clone(c), s.Seq
1105		mu.Unlock()
1106		f, w, _ := os.Pipe()
1107		_, _ = w.Write([]byte{1})
1108		w.Close()
1109		return f, nil
1110	})
1111	r.EXPECT().Close().Return().Once()
1112	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
1113	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
1114	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
1115	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
1116	// drawn waits for a frame of scene seq and returns what it drew for
1117	// window 1.
1118	drawn := func(seq uint64, cond func(ports.SurfaceContent, bool) bool) (ports.SurfaceContent, bool) {
1119		t.Helper()
1120		var c ports.SurfaceContent
1121		var ok bool
1122		waitFor(t, func() bool {
1123			mu.Lock()
1124			defer mu.Unlock()
1125			c, ok = last[1]
1126			return lastSeq == seq && cond(c, ok)
1127		})
1128		return c, ok
1129	}
1130	scenes := make(chan ports.Scene, 1)
1131	contents := make(chan ports.SurfaceContent, 1)
1132	ctx, cancel := context.WithCancel(context.Background())
1133	done := make(chan error, 1)
1134	go func() {
1135		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, make(chan ports.OutputPresented, 64), nil, nil)
1136	}()
1137	acq, acqW, _ := os.Pipe()
1138	defer acq.Close()
1139	defer acqW.Close()
1140	win := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 10, H: 10}}
1141	scenes <- ports.Scene{Seq: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}}
1142	full := ports.SurfaceContent{ID: 1, Seq: 1, Width: 10, Height: 10, DMABuf: &ports.DMABuf{ID: 5, Planes: []ports.DMABufPlane{{}}}, Acquire: acq}
1143	contents <- full
1144	drawn(1, func(c ports.SurfaceContent, ok bool) bool { return ok && c.Acquire == acq })
1145	// The window unmaps: its empty content arrives before the scene that
1146	// draws it leaving (fading). The last content stays, fence dropped.
1147	contents <- ports.SurfaceContent{ID: 1, Seq: 2}
1148	kept, _ := drawn(1, func(c ports.SurfaceContent, ok bool) bool { return !ok || c.Acquire == nil })
1149	if kept.DMABuf != full.DMABuf || kept.Seq != 1 {
1150		t.Fatalf("frame after the empty content drew %+v, want the last content", kept)
1151	}
1152	// A leaving frame: still drawn from the kept content.
1153	win.Fade = 0.5
1154	scenes <- ports.Scene{Seq: 2, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}}
1155	if c, ok := drawn(2, func(ports.SurfaceContent, bool) bool { return true }); !ok || c.DMABuf != full.DMABuf {
1156		t.Fatalf("the leaving frame drew %+v (ok %t), want the kept content", c, ok)
1157	}
1158	// The scene no longer lists it: gone.
1159	scenes <- ports.Scene{Seq: 3, OutputWidth: 200, OutputHeight: 100}
1160	if c, ok := drawn(3, func(ports.SurfaceContent, bool) bool { return true }); ok {
1161		t.Fatalf("the kept content %+v survived a scene without the window", c)
1162	}
1163	cancel()
1164	if err := <-done; err != nil {
1165		t.Fatal(err)
1166	}
1167}

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