v0.3.0 cf370a0

output_test.go RawHistoryFolder

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

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 25

Tags 10

↑↓ moveenter openesc close