main f2d166b

release_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"errors"
6	"image"
7	"os"
8	"testing"
9	"time"
10
11	"github.com/bnema/go-wayland-bindings/server/fifo"
12	"github.com/bnema/go-wayland-bindings/server/wayland"
13	"github.com/bnema/neferwl/internal/adapters/outputkit/capture"
14	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
15	"github.com/bnema/neferwl/internal/ports"
16	"github.com/stretchr/testify/mock"
17	"github.com/stretchr/testify/require"
18)
19
20// A cached child buffer is not released until its role is destroyed;
21// nothing in the output could have sampled an unpublished commit.
22func TestCachedSubsurfaceBufferReleaseOnRoleDestroy(t *testing.T) {
23	s, events, _, contents, dir := contentServer(t)
24	c := protocolClient(t, s, dir)
25	_, root, _ := surfaceMapper(t, c, events)()
26	drainContents(contents)
27	comp := bindProtocol(t, c, "wl_compositor")
28	subc := bindProtocol(t, c, "wl_subcompositor")
29	child := c.AllocateID()
30	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
31	registerProtocol(t, c, child)
32	sub := c.AllocateID()
33	requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, root)
34	registerProtocol(t, c, sub)
35	b := shmBuffer(t, c)
36	p := &syncReleaseProxy{released: make(chan struct{}, 4)}
37	p.SetID(b)
38	registerWireProxy(c, p)
39	requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
40	requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
41	if err := c.Roundtrip(); err != nil {
42		t.Fatal(err)
43	}
44	select {
45	case <-p.released:
46		t.Fatal("cached child buffer released before parent commit")
47	case <-time.After(40 * time.Millisecond):
48	}
49	requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
50	if err := c.Roundtrip(); err != nil {
51		t.Fatal(err)
52	}
53	select {
54	case <-p.released:
55	case <-time.After(2 * time.Second):
56		t.Fatal("dropped child buffer never released")
57	}
58}
59
60func TestKeepHeld(t *testing.T) {
61	now := time.Unix(100, 0)
62	dma := heldBuffer{window: 1, id: 7, after: 5, at: now}
63	shm := heldBuffer{window: 1, after: 5, at: now}
64	out := func(shown, queued, seq uint64) ports.OutputPresented {
65		return ports.OutputPresented{Shown: shown, Queued: queued, Seen: map[ports.WindowID]uint64{1: seq}}
66	}
67	type R = []ports.OutputPresented
68	for _, tc := range []struct {
69		name    string
70		h       heldBuffer
71		reports R
72		after   time.Duration
73		want    bool
74	}{
75		{"no report yet", dma, nil, 0, true},
76		{"no report times out", dma, nil, heldTimeout, false},
77		{"output has only this content", dma, R{out(0, 0, 5)}, 0, true},
78		{"later content seen", dma, R{out(0, 0, 6)}, 0, false},
79		{"scanned out", dma, R{out(7, 0, 6)}, time.Second, true},
80		{"queued for scanout", dma, R{out(0, 7, 6)}, time.Second, true},
81		{"another buffer scanned out", dma, R{out(8, 0, 6)}, 0, false},
82		{"silent output times out", dma, R{out(0, 0, 5)}, heldTimeout, false},
83		{"shm follows content", shm, R{out(0, 0, 6)}, 0, false},
84		// A window moved off output A: A still scans the buffer out.
85		{"other output scans it out", dma, R{out(0, 0, 6), out(7, 0, 6)}, time.Second, true},
86		{"other output lags", dma, R{out(0, 0, 6), out(0, 0, 5)}, 0, true},
87		{"all outputs moved on", dma, R{out(0, 0, 6), out(0, 0, 6)}, 0, false},
88	} {
89		if got := keepHeld(tc.h, tc.reports, now.Add(tc.after)); got != tc.want {
90			t.Errorf("%s: %v", tc.name, got)
91		}
92	}
93}
94
95// Child reads on a separate device outlive the stale display report timeout.
96// The same predicate protects wl_buffer.release and explicit release points.
97func TestKeepHeldChildReadsBeyondTimeout(t *testing.T) {
98	now := time.Unix(100, 0)
99	report := ports.OutputPresented{
100		Seen:       map[ports.WindowID]uint64{1: 20, 2: 20},
101		ChildReads: map[ports.WindowID]uint64{1: 5},
102	}
103	for _, id := range []uint64{0, 7} { // SHM / explicit points only, and dmabuf
104		h := heldBuffer{window: 1, id: id, after: 5, at: now}
105		if !keepHeld(h, []ports.OutputPresented{report}, now.Add(time.Second)) {
106			t.Fatal("unfinished child read released after 100ms")
107		}
108		h.window = 2
109		if keepHeld(h, []ports.OutputPresented{report}, now.Add(time.Second)) {
110			t.Fatal("unrelated display window held by child")
111		}
112		h.window, h.after = 1, 4
113		if keepHeld(h, []ports.OutputPresented{report}, now.Add(time.Second)) {
114			t.Fatal("buffer older than child's content held")
115		}
116		h.after = 5
117		finished := report
118		finished.ChildReads = nil // render completed, or device closed
119		if keepHeld(h, []ports.OutputPresented{finished}, now.Add(time.Second)) {
120			t.Fatal("finished child read still holds buffer")
121		}
122	}
123	allocs := testing.AllocsPerRun(100, func() {
124		keepHeld(heldBuffer{window: 1, after: 5, at: now}, []ports.OutputPresented{report}, now.Add(time.Second))
125	})
126	if allocs != 0 {
127		t.Fatalf("child release guard: %.1f allocs, want 0", allocs)
128	}
129	t.Logf("child release guard: %.1f allocs", allocs)
130}
131
132// An actual unfinished child fence protects releases after the stale timeout,
133// even when the display's Seen is unchanged (so no cap was necessary).
134func TestKeepHeldUntilChildFenceCompletes(t *testing.T) {
135	read, write, err := os.Pipe()
136	require.NoError(t, err)
137	defer write.Close()
138	child := portsmocks.NewMockRenderer(t)
139	child.EXPECT().Render(mock.Anything, mock.Anything).Return(read, nil).Once()
140	// A failed readback does not finish the render's client-buffer reads.
141	child.EXPECT().BeginCapture().Return(nil, errors.New("readback unavailable")).Once()
142	child.EXPECT().Close().Return().Once()
143	p := capture.NewPipeline(context.Background(), make(chan ports.CaptureDone, 1))
144	defer p.Close(nil)
145	p.EnableOffscreen(func(int, int) (ports.Renderer, error) { return child, nil })
146	s := ports.Scene{
147		Capture:      &ports.SceneCapture{Workspace: 9},
148		CaptureScene: &ports.Scene{OutputWidth: 2, OutputHeight: 2, Scale: 1, Windows: []ports.SceneWindow{{ID: 1}}},
149	}
150	p.SubmitHidden(s, map[ports.WindowID]ports.SurfaceContent{1: {Seq: 5}}, []ports.CaptureRequest{{
151		ID: 1, Workspace: 9, OffScreen: true, Region: image.Rect(0, 0, 2, 2),
152	}})
153	now := time.Unix(100, 0)
154	h := heldBuffer{window: 1, after: 5, at: now}
155	seen := map[ports.WindowID]uint64{1: 5}
156	got, reads, pending := p.CapHiddenSeen(seen)
157	require.True(t, pending)
158	require.Equal(t, seen, got)
159	reports := []ports.OutputPresented{{Seen: got, ChildReads: reads}}
160	require.True(t, keepHeld(h, reports, now.Add(time.Second)), "unsignalled fence outlives 100ms timeout")
161	_, err = write.Write([]byte{1})
162	require.NoError(t, err)
163	got, reads, pending = p.CapHiddenSeen(seen)
164	require.False(t, pending)
165	reports[0] = ports.OutputPresented{Seen: got, ChildReads: reads}
166	require.False(t, keepHeld(h, reports, now.Add(time.Second)), "signalled fence lifts child hold")
167}
168
169// A buffer replaced just after its window becomes invisible is still held:
170// a scanout plane may read it until the flip that hides the window.
171func TestInvisibleWindowBufferHeldUntilReport(t *testing.T) {
172	s, events, commands, contents, dir := contentServer(t)
173	go func() {
174		for range contents {
175		}
176	}()
177	c := protocolClient(t, s, dir)
178	w, root, xdg := surfaceMapper(t, c, events)()
179	// Our configures must not block the mapper's one-slot proxy.
180	registerProtocol(t, c, xdg)
181	// Commands apply asynchronously: wait for each configure.
182	configure := func(v ports.ConfigureWindow) {
183		t.Helper()
184		commands <- v
185		deadline := time.Now().Add(2 * time.Second)
186		for {
187			applied := false
188			s.display.Do(func() { win := s.windows[w.ID]; applied = win.hasLast && win.last == v })
189			if applied {
190				return
191			}
192			if time.Now().After(deadline) {
193				t.Fatalf("configure not applied: %+v", v)
194			}
195			time.Sleep(time.Millisecond)
196		}
197	}
198	configure(ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: true})
199	b, other := shmBuffer(t, c), shmBuffer(t, c)
200	attach := func(buf uint32) {
201		requestProtocol(t, c, root, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
202		requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
203	}
204	attach(b)
205	if err := c.Roundtrip(); err != nil {
206		t.Fatal(err)
207	}
208	configure(ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1"})
209	attach(other)
210	if err := c.Roundtrip(); err != nil {
211		t.Fatal(err)
212	}
213	s.display.Do(func() {
214		held := false
215		for _, h := range s.held {
216			held = held || h.res != nil && h.res.ID() == b
217		}
218		if !held {
219			t.Errorf("replaced buffer of an invisible window released at once: held=%d", len(s.held))
220		}
221		// Without a report showing it, the hold still ends.
222		s.releaseHeld(time.Now().Add(heldTimeout), nil)
223		for _, h := range s.held {
224			if h.res != nil && h.res.ID() == b {
225				t.Error("held buffer of an invisible window never released")
226			}
227		}
228	})
229}
230
231// A later queued use of the same wl_buffer still owns it after the output
232// acknowledges the intervening commit.
233func TestHeldBufferRequeuedBeforeOutputAck(t *testing.T) {
234	s, events, commands, contents, dir := contentServer(t)
235	c := protocolClient(t, s, dir)
236	w, root, _ := surfaceMapper(t, c, events)()
237	commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: true}
238	if err := c.Roundtrip(); err != nil {
239		t.Fatal(err)
240	}
241	manager := bindProtocol(t, c, "wp_fifo_manager_v1")
242	f := c.AllocateID()
243	registerProtocol(t, c, f)
244	requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, root)
245	b, other := shmBuffer(t, c), shmBuffer(t, c)
246	p := &syncReleaseProxy{released: make(chan struct{}, 4)}
247	p.SetID(b)
248	registerWireProxy(c, p)
249	attach := func(buf uint32) {
250		requestProtocol(t, c, root, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
251		requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
252	}
253	attach(b)
254	if err := c.Roundtrip(); err != nil {
255		t.Fatal(err)
256	}
257	drainContents(contents)
258	requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
259	attach(other)
260	requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
261	attach(b)
262	if err := c.Roundtrip(); err != nil {
263		t.Fatal(err)
264	}
265	s.display.Do(func() {
266		surf := s.windows[w.ID].xdg.surface
267		if len(surf.queue) != 1 || surf.queue[0].buffer == nil || sameBuffer(surf.queue[0].buffer, surf.current) || len(s.held) == 0 {
268			t.Errorf("expected replaced buffer and queued reuse: queue=%d held=%d", len(surf.queue), len(s.held))
269		}
270		s.reports["HEADLESS-1"] = ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{w.ID: ^uint64(0)}}
271		s.releaseHeld(time.Now(), nil)
272		if len(s.held) == 0 {
273			t.Error("released held buffer despite queued reuse")
274		}
275		surf.barrier = false
276		s.tickFifo(time.Now(), nil)
277		s.releaseHeld(time.Now().Add(heldTimeout), nil)
278		if len(s.held) != 0 {
279			t.Error("held buffer not released after queued reuse applied")
280		}
281	})
282	select {
283	case <-p.released:
284		t.Fatal("queued buffer released after output ack")
285	case <-time.After(40 * time.Millisecond):
286	}
287}

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