1package wayland
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/wayland"
8 "github.com/bnema/neferwl/internal/adapters/vulkan"
9 "github.com/bnema/neferwl/internal/ports"
10 "github.com/bnema/wlturbo"
11 "golang.org/x/sys/unix"
12)
13
14// callbackBudgetProxy observes real wl_surface.frame replies.
15type callbackBudgetProxy struct {
16 wlturbo.BaseProxy
17 done chan struct{}
18}
19
20func (p *callbackBudgetProxy) Dispatch(e *wlturbo.Event) {
21 if e.Opcode == uint16(wayland.CallbackEventDone) {
22 p.done <- struct{}{}
23 }
24}
25
26// TestHeadlessTiledSHMCallbackAndCopyBudget guards callback delivery and
27// the GPU copy budget across publications with many stable SHM children.
28func TestHeadlessTiledSHMCallbackAndCopyBudget(t *testing.T) {
29 s, events, _, contents, dir := contentServer(t)
30 c := protocolClient(t, s, dir)
31 win, root, _ := surfaceMapper(t, c, events)()
32 comp := bindProtocol(t, c, "wl_compositor")
33 subc := bindProtocol(t, c, "wl_subcompositor")
34 shm := bindProtocol(t, c, "wl_shm")
35 registerProtocol(t, c, shm)
36 fd, err := unix.MemfdCreate("tiled-playback", unix.MFD_CLOEXEC)
37 if err != nil {
38 t.Fatal(err)
39 }
40 defer unix.Close(fd)
41 const tiles = 64
42 const edge = 8
43 const stride = edge * 4
44 const size = (tiles + 1) * edge * stride
45 if err := unix.Ftruncate(fd, size); err != nil {
46 t.Fatal(err)
47 }
48 pool := c.AllocateID()
49 registerProtocol(t, c, pool)
50 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(size)); err != nil {
51 t.Fatal(err)
52 }
53 children := make([]uint32, tiles+1)
54 buffers := make([]uint32, tiles+1)
55 for i := range children {
56 children[i] = c.AllocateID()
57 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, children[i])
58 registerProtocol(t, c, children[i])
59 sub := c.AllocateID()
60 requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, children[i], root)
61 registerProtocol(t, c, sub)
62 if i < tiles {
63 requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(i%8*edge), int32(i/8*edge))
64 } else {
65 requestProtocol(t, c, sub, wayland.SubsurfaceRequestSetPosition, int32(80), int32(0))
66 }
67 buffers[i] = c.AllocateID()
68 registerProtocol(t, c, buffers[i])
69 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffers[i], int32(i*edge*stride), int32(edge), int32(edge), int32(stride), uint32(0))
70 requestProtocol(t, c, children[i], wayland.SurfaceRequestAttach, buffers[i], int32(0), int32(0))
71 requestProtocol(t, c, children[i], wayland.SurfaceRequestCommit)
72 }
73 requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
74 if err := c.Roundtrip(); err != nil {
75 t.Fatal(err)
76 }
77 wait := func() ports.SurfaceContent {
78 t.Helper()
79 deadline := time.After(5 * time.Second)
80 for {
81 select {
82 case got := <-contents:
83 if got.ID == win.ID && len(got.Children) == tiles+1 {
84 return got
85 }
86 case <-deadline:
87 t.Fatal("no tiled publication")
88 }
89 }
90 }
91 previous := wait()
92 r, err := vulkan.New(128, 128)
93 if err != nil {
94 t.Skipf("Vulkan unavailable: %v", err)
95 }
96 defer r.Close()
97 scene := ports.Scene{Windows: []ports.SceneWindow{{ID: win.ID, Rect: ports.Rect{W: 128, H: 128}}}}
98 render := func(content ports.SurfaceContent) {
99 t.Helper()
100 fence, err := r.Render(scene, map[ports.WindowID]ports.SurfaceContent{win.ID: content})
101 if fence != nil {
102 _ = fence.Close()
103 }
104 if err != nil {
105 t.Fatal(err)
106 }
107 }
108 render(previous)
109 copied := r.CopiedBytes()
110 for i := range 120 {
111 done := make(chan struct{}, 1)
112 cb := c.AllocateID()
113 p := &callbackBudgetProxy{done: done}
114 p.SetID(cb)
115 registerWireProxy(c, p)
116 requestProtocol(t, c, children[tiles], wayland.SurfaceRequestFrame, cb)
117 requestProtocol(t, c, children[tiles], wayland.SurfaceRequestAttach, buffers[tiles], int32(0), int32(0))
118 requestProtocol(t, c, children[tiles], wayland.SurfaceRequestCommit)
119 requestProtocol(t, c, root, wayland.SurfaceRequestCommit)
120 if err := c.Roundtrip(); err != nil {
121 t.Fatal(err)
122 }
123 deadline := time.After(3 * time.Second)
124 for {
125 select {
126 case <-done:
127 goto callbackDone
128 default:
129 }
130 result := make(chan error, 1)
131 go func() { result <- c.Dispatch() }()
132 select {
133 case err := <-result:
134 if err != nil {
135 t.Fatal(err)
136 }
137 case <-deadline:
138 _ = c.Close()
139 t.Fatalf("callback %d not delivered", i)
140 }
141 }
142 callbackDone:
143 next := wait()
144 render(next)
145 if delta := r.CopiedBytes() - copied; delta != edge*edge*4 {
146 t.Fatalf("frame %d copied %d bytes, want only the changing child's %d (root: id=%d version=%d size=%dx%d, previous root version=%d; first tile: id=%d version=%d previous=%d)", i, delta, edge*edge*4, next.Surface, next.Version, next.Width, next.Height, previous.Version, next.Children[0].Surface, next.Children[0].Version, previous.Children[0].Version)
147 }
148 copied = r.CopiedBytes()
149 for tile := range tiles {
150 if next.Children[tile].Version != previous.Children[tile].Version {
151 t.Fatalf("unchanged tile %d changed version", tile)
152 }
153 }
154 previous = next
155 }
156}