1package wayland
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/wayland"
8 "github.com/bnema/go-wayland-bindings/server/wlrlayershell"
9 "github.com/bnema/neferwl/internal/ports"
10 "github.com/bnema/wlturbo"
11)
12
13var second = ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-2", Width: 1280, Height: 1024}, X: 1920, Width: 1280, Height: 1024, Scale: 1}
14
15func TestOutputHDRConfirmedState(t *testing.T) {
16 s := &Server{}
17 const name = "DP-1"
18 if _, ok := s.hdrOutputs[name]; ok {
19 t.Fatal("output must start SDR")
20 }
21 want := ports.OutputHDR{MaxLuminance: 1000, MaxFrameAverage: 400, MinLuminance: 0.005}
22 s.setOutputHDR(ports.OutputFormats{Output: name, HDR: &want})
23 if got := s.hdrOutputs[name]; got != want {
24 t.Fatalf("HDR report: %+v", got)
25 }
26 s.setOutputHDR(ports.OutputFormats{Output: name})
27 if _, ok := s.hdrOutputs[name]; ok {
28 t.Fatal("SDR fallback must clear HDR")
29 }
30}
31
32// outputCount counts wl_output globals the client sees.
33func outputCount(c *wlturbo.Display) int {
34 n := 0
35 for _, g := range c.Registry().GetGlobals() {
36 if g.Interface == "wl_output" {
37 n++
38 }
39 }
40 return n
41}
42
43// waitOutputs round-trips until the client sees n wl_output globals.
44func waitOutputs(t *testing.T, c *wlturbo.Display, n int) {
45 t.Helper()
46 for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
47 if err := c.Roundtrip(); err != nil {
48 t.Fatal(err)
49 }
50 if outputCount(c) == n {
51 return
52 }
53 time.Sleep(5 * time.Millisecond)
54 }
55 t.Fatalf("wl_output globals: %d, want %d", outputCount(c), n)
56}
57
58type closedProxy struct {
59 wlturbo.BaseProxy
60 configured chan [3]uint32
61 closed chan struct{}
62}
63
64func (p *closedProxy) Dispatch(e *wlturbo.Event) {
65 switch e.Opcode {
66 case uint16(wlrlayershell.ZwlrLayerSurfaceV1EventConfigure):
67 p.configured <- [3]uint32{e.Uint32(), e.Uint32(), e.Uint32()}
68 case uint16(wlrlayershell.ZwlrLayerSurfaceV1EventClosed):
69 close(p.closed)
70 }
71}
72
73// wl_fixes: a client acknowledges a removed output global and destroys its
74// registry; a bogus acknowledgement is a protocol error.
75func TestFixesAckAndDestroyRegistry(t *testing.T) {
76 s, _, commands, dir := lifecycleServer(t)
77 c := protocolClient(t, s, dir)
78 g, ok := c.Registry().FindGlobal("wl_fixes")
79 if !ok || g.Version != 2 {
80 t.Fatalf("wl_fixes %+v", g)
81 }
82 fixes := bindVersion(t, c, "wl_fixes", 2)
83 commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0], second}, Focused: "HEADLESS-1"}
84 waitOutputs(t, c, 2)
85 var name uint32
86 for n, g := range c.Registry().GetGlobals() {
87 if g.Interface == "wl_output" && n > name {
88 name = n
89 }
90 }
91 commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0]}, Focused: "HEADLESS-1"}
92 waitOutputs(t, c, 1)
93 requestProtocol(t, c, fixes, wayland.FixesRequestAckGlobalRemove, c.Registry().ID(), name)
94 requestProtocol(t, c, fixes, wayland.FixesRequestDestroyRegistry, c.Registry().ID())
95 // wl_fixes destroys another object, not the request's own proxy.
96 c.Context().UnregisterID(c.Registry().ID())
97 if err := c.Roundtrip(); err != nil {
98 t.Fatalf("valid ack and destroy: %v", err)
99 }
100
101 // A global that is still announced cannot be acknowledged.
102 c2 := protocolClient(t, s, dir)
103 fixes2 := bindVersion(t, c2, "wl_fixes", 2)
104 out, _ := c2.Registry().FindGlobal("wl_output")
105 requestProtocol(t, c2, fixes2, wayland.FixesRequestAckGlobalRemove, c2.Registry().ID(), out.Name)
106 if err := c2.Roundtrip(); err == nil {
107 t.Fatal("ack of a live global accepted")
108 }
109}
110
111func TestOutputHotplugAndLayerOutput(t *testing.T) {
112 s, _, commands, dir := lifecycleServer(t)
113 c := protocolClient(t, s, dir)
114 if outputCount(c) != 1 {
115 t.Fatal(outputCount(c))
116 }
117 commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0], second}, Focused: "HEADLESS-1"}
118 waitOutputs(t, c, 2)
119 // A layer surface on the second output is sized from it.
120 var name uint32
121 for n, g := range c.Registry().GetGlobals() {
122 if g.Interface == "wl_output" && (name == 0 || n > name) {
123 name = n
124 }
125 }
126 out, err := bindWireID(c, name, "wl_output", 4)
127 if err != nil {
128 t.Fatal(err)
129 }
130 registerProtocol(t, c, out)
131 comp := bindProtocol(t, c, "wl_compositor")
132 shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
133 surf := c.AllocateID()
134 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surf)
135 registerProtocol(t, c, surf)
136 layer := c.AllocateID()
137 proxy := &closedProxy{configured: make(chan [3]uint32, 2), closed: make(chan struct{})}
138 proxy.SetID(layer)
139 registerWireProxy(c, proxy)
140 requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, layer, surf, out, uint32(ports.LayerTop), "bar")
141 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetAnchor, uint32(13))
142 requestProtocol(t, c, layer, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(0), uint32(30))
143 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
144 if err := c.Roundtrip(); err != nil {
145 t.Fatal(err)
146 }
147 select {
148 case cfg := <-proxy.configured:
149 if cfg[1] != 1280 {
150 t.Fatalf("configure %v", cfg)
151 }
152 case <-time.After(2 * time.Second):
153 t.Fatal("configure timeout")
154 }
155 // Unplugged: the global goes away and the layer surface is closed.
156 commands <- ports.SetOutputs{Outputs: ports.Layout{testOutputs[0]}, Focused: "HEADLESS-1"}
157 waitOutputs(t, c, 1)
158 select {
159 case <-proxy.closed:
160 case <-time.After(2 * time.Second):
161 t.Fatal("layer not closed")
162 }
163}