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}