1package wayland
2
3import (
4 "context"
5 "testing"
6
7 cm "github.com/bnema/go-wayland-bindings/server/colormanagement"
8 "github.com/bnema/go-wayland-bindings/server/wayland"
9 "github.com/bnema/neferwl/internal/logging"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/wlturbo"
12)
13
14type colorEvent struct {
15 wlturbo.BaseProxy
16 events []uint16
17 values [][]uint32
18}
19
20func (p *colorEvent) Dispatch(e *wlturbo.Event) {
21 p.events = append(p.events, e.Opcode)
22 var vals []uint32
23 for i := 0; i < 12; i++ {
24 if len(e.Data())-e.Offset() < 4 {
25 break
26 }
27 vals = append(vals, e.Uint32())
28 }
29 p.values = append(p.values, vals)
30}
31func watchColor(c *wlturbo.Display, id uint32) *colorEvent {
32 p := &colorEvent{}
33 p.SetID(id)
34 registerWireProxy(c, p)
35 return p
36}
37func colorServer(t *testing.T) (*Server, *wlturbo.Display, chan ports.OutputFormats) {
38 t.Helper()
39 dir := t.TempDir()
40 formats := make(chan ports.OutputFormats, 8)
41 s, err := New(Options{RuntimeDir: dir, Outputs: ports.Layout{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 8, Height: 8}, Width: 8, Height: 8, Scale: 1}}}, Channels{OutputFormats: formats}, logging.For(context.Background(), "wayland"))
42 if err != nil {
43 t.Fatal(err)
44 }
45 ctx, cancel := context.WithCancel(context.Background())
46 done := make(chan error, 1)
47 go func() { done <- s.Run(ctx) }()
48 t.Cleanup(func() {
49 cancel()
50 if err := <-done; err != nil {
51 t.Error(err)
52 }
53 })
54 return s, protocolClient(t, s, dir), formats
55}
56func TestColorMesaFlow(t *testing.T) {
57 s, c, _ := colorServer(t)
58 manager := bindVersion(t, c, "wp_color_manager_v1", 2)
59 caps := watchColor(c, manager)
60 if err := c.Roundtrip(); err != nil {
61 t.Fatal(err)
62 }
63 var features, tf, prim []uint32
64 for i, op := range caps.events {
65 switch uint32(op) {
66 case cm.WpColorManagerV1EventSupportedFeature:
67 features = append(features, caps.values[i][0])
68 case cm.WpColorManagerV1EventSupportedTfNamed:
69 tf = append(tf, caps.values[i][0])
70 case cm.WpColorManagerV1EventSupportedPrimariesNamed:
71 prim = append(prim, caps.values[i][0])
72 }
73 }
74 if len(features) != 1 || features[0] != uint32(cm.WpColorManagerV1FeatureParametric) || len(tf) != 3 || tf[0] != pq || tf[1] != extLinear || tf[2] != uint32(cm.WpColorManagerV1TransferFunctionSrgb) || len(prim) != 2 || uint32(caps.events[len(caps.events)-1]) != cm.WpColorManagerV1EventDone {
75 t.Fatalf("capabilities %v %v %v %v", features, tf, prim, caps.events)
76 }
77 comp := bindProtocol(t, c, "wl_compositor")
78 wl := c.AllocateID()
79 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, wl)
80 registerProtocol(t, c, wl)
81 surfaceID := c.AllocateID()
82 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestGetSurface, surfaceID, wl)
83 registerProtocol(t, c, surfaceID)
84 creator := c.AllocateID()
85 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestCreateParametricCreator, creator)
86 registerProtocol(t, c, creator)
87 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetPrimariesNamed, bt2020)
88 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetTfNamed, pq)
89 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetMaxCll, uint32(1000))
90 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetMaxFall, uint32(400))
91 image := c.AllocateID()
92 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestCreate, image)
93 ready := watchColor(c, image)
94 if err := c.Roundtrip(); err != nil {
95 t.Fatal(err)
96 }
97 if len(ready.events) != 1 || uint32(ready.events[0]) != cm.WpImageDescriptionV1EventReady2 {
98 t.Fatalf("ready %v", ready.events)
99 }
100 requestProtocol(t, c, surfaceID, cm.WpColorManagementSurfaceV1RequestSetImageDescription, image, uint32(0))
101 if err := c.Roundtrip(); err != nil {
102 t.Fatal(err)
103 }
104 var before SurfaceColor
105 s.display.Do(func() { before = s.surfaceColor(wl) })
106 if before.Set {
107 t.Fatal("color applied before commit")
108 }
109 requestProtocol(t, c, wl, wayland.SurfaceRequestCommit)
110 if err := c.Roundtrip(); err != nil {
111 t.Fatal(err)
112 }
113 s.display.Do(func() { before = s.surfaceColor(wl) })
114 if before != (SurfaceColor{true, pq, bt2020, 1000, 400}) {
115 t.Fatalf("committed color %+v", before)
116 }
117 var routed ports.SurfaceColor
118 s.display.Do(func() {
119 for res, surf := range s.surfaces {
120 if res.ID() == wl {
121 routed = surf.tree(1).Color
122 }
123 }
124 })
125 if routed != (ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020, MaxCLL: 1000, MaxFALL: 400}) {
126 t.Fatalf("routed color %+v", routed)
127 }
128 requestProtocol(t, c, surfaceID, cm.WpColorManagementSurfaceV1RequestUnsetImageDescription)
129 requestProtocol(t, c, wl, wayland.SurfaceRequestCommit)
130 if err := c.Roundtrip(); err != nil {
131 t.Fatal(err)
132 }
133 s.display.Do(func() { before = s.surfaceColor(wl) })
134 if before.Set {
135 t.Fatal("unset not committed")
136 }
137}
138
139// v3 advertises windows_bt2100; its description is PQ BT.2020, applies to
140// a surface, and allows no get_information.
141func TestColorWindowsBT2100(t *testing.T) {
142 s, c, _ := colorServer(t)
143 manager := bindVersion(t, c, "wp_color_manager_v1", 3)
144 caps := watchColor(c, manager)
145 if err := c.Roundtrip(); err != nil {
146 t.Fatal(err)
147 }
148 var bt2100 bool
149 for i, op := range caps.events {
150 if uint32(op) == cm.WpColorManagerV1EventSupportedFeature && caps.values[i][0] == uint32(cm.WpColorManagerV1FeatureWindowsBt2100) {
151 bt2100 = true
152 }
153 }
154 if !bt2100 {
155 t.Fatalf("windows_bt2100 not advertised: %v", caps.values)
156 }
157 comp := bindProtocol(t, c, "wl_compositor")
158 wl := c.AllocateID()
159 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, wl)
160 registerProtocol(t, c, wl)
161 surfaceID := c.AllocateID()
162 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestGetSurface, surfaceID, wl)
163 registerProtocol(t, c, surfaceID)
164 image := c.AllocateID()
165 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestCreateWindowsBt2100, image)
166 ready := watchColor(c, image)
167 if err := c.Roundtrip(); err != nil {
168 t.Fatal(err)
169 }
170 if len(ready.events) != 1 || uint32(ready.events[0]) != cm.WpImageDescriptionV1EventReady2 {
171 t.Fatalf("ready %v", ready.events)
172 }
173 requestProtocol(t, c, surfaceID, cm.WpColorManagementSurfaceV1RequestSetImageDescription, image, uint32(cm.WpColorManagerV1RenderIntentPerceptual))
174 requestProtocol(t, c, wl, wayland.SurfaceRequestCommit)
175 if err := c.Roundtrip(); err != nil {
176 t.Fatal(err)
177 }
178 var got SurfaceColor
179 s.display.Do(func() { got = s.surfaceColor(wl) })
180 if got != (SurfaceColor{Set: true, TF: pq, Primaries: bt2020}) {
181 t.Fatalf("committed color %+v", got)
182 }
183 info := c.AllocateID()
184 requestProtocol(t, c, image, cm.WpImageDescriptionV1RequestGetInformation, info)
185 if err := c.Roundtrip(); err == nil {
186 t.Fatal("get_information allowed")
187 }
188}
189
190func (s *Server) surfaceColor(id uint32) SurfaceColor {
191 for r, v := range s.surfaces {
192 if r.ID() == id {
193 return v.ColorDescription()
194 }
195 }
196 return SurfaceColor{}
197}
198func TestColorOutputAndFeedback(t *testing.T) {
199 s, c, _ := colorServer(t)
200 manager := bindVersion(t, c, "wp_color_manager_v1", 2)
201 registerProtocol(t, c, manager)
202 var name uint32
203 for id, g := range c.Registry().GetGlobals() {
204 if g.Interface == "wl_output" {
205 name = id
206 }
207 }
208 out, err := bindWireID(c, name, "wl_output", 4)
209 if err != nil {
210 t.Fatal(err)
211 }
212 registerProtocol(t, c, out)
213 obj := c.AllocateID()
214 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestGetOutput, obj, out)
215 changed := watchColor(c, obj)
216 desc := c.AllocateID()
217 requestProtocol(t, c, obj, cm.WpColorManagementOutputV1RequestGetImageDescription, desc)
218 ready := watchColor(c, desc)
219 if err := c.Roundtrip(); err != nil {
220 t.Fatal(err)
221 }
222 if len(ready.events) != 1 || uint32(ready.events[0]) != cm.WpImageDescriptionV1EventReady2 {
223 t.Fatalf("output ready %v", ready.events)
224 }
225 oldID := uint64(ready.values[0][0])<<32 | uint64(ready.values[0][1])
226 if oldID == 0 {
227 t.Fatal("zero output identity")
228 }
229 info := c.AllocateID()
230 requestProtocol(t, c, desc, cm.WpImageDescriptionV1RequestGetInformation, info)
231 ev := watchColor(c, info)
232 if err := c.Roundtrip(); err != nil {
233 t.Fatal(err)
234 }
235 if len(ev.events) < 5 || ev.values[1][0] != srgb || ev.values[2][0] != uint32(cm.WpColorManagerV1TransferFunctionSrgb) {
236 t.Fatalf("SDR info %v %v", ev.events, ev.values)
237 }
238 comp := bindProtocol(t, c, "wl_compositor")
239 wl := c.AllocateID()
240 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, wl)
241 registerProtocol(t, c, wl)
242 feedback := c.AllocateID()
243 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestGetSurfaceFeedback, feedback, wl)
244 pref := watchColor(c, feedback)
245 if err := c.Roundtrip(); err != nil {
246 t.Fatal(err)
247 }
248 preferredID := c.AllocateID()
249 requestProtocol(t, c, feedback, cm.WpColorManagementSurfaceFeedbackV1RequestGetPreferredParametric, preferredID)
250 preferred := watchColor(c, preferredID)
251 if err := c.Roundtrip(); err != nil {
252 t.Fatal(err)
253 }
254 if len(preferred.events) != 1 || uint32(preferred.events[0]) != cm.WpImageDescriptionV1EventReady2 {
255 t.Fatalf("preferred %v", preferred.events)
256 }
257 s.display.Do(func() {
258 s.setOutputHDR(ports.OutputFormats{Output: "HEADLESS-1", HDR: &ports.OutputHDR{MaxLuminance: 1000, MaxFrameAverage: 400, MinLuminance: .005}})
259 })
260 if err := c.Roundtrip(); err != nil {
261 t.Fatal(err)
262 }
263 // The format report is asynchronous to client requests.
264 if err := c.Roundtrip(); err != nil {
265 t.Fatal(err)
266 }
267 if len(changed.events) == 0 || len(pref.events) == 0 {
268 t.Fatalf("change events output=%v preferred=%v", changed.events, pref.events)
269 }
270 desc2 := c.AllocateID()
271 requestProtocol(t, c, obj, cm.WpColorManagementOutputV1RequestGetImageDescription, desc2)
272 ready2 := watchColor(c, desc2)
273 if err := c.Roundtrip(); err != nil {
274 t.Fatal(err)
275 }
276 newID := uint64(ready2.values[0][0])<<32 | uint64(ready2.values[0][1])
277 if newID == 0 || newID == oldID {
278 t.Fatalf("identities before=%d after=%d", oldID, newID)
279 }
280 if len(pref.values) == 0 || uint64(pref.values[len(pref.values)-1][0])<<32|uint64(pref.values[len(pref.values)-1][1]) != newID {
281 t.Fatalf("preferred identity mismatch: %v, ready=%d", pref.values, newID)
282 }
283 info2 := c.AllocateID()
284 requestProtocol(t, c, desc2, cm.WpImageDescriptionV1RequestGetInformation, info2)
285 hdr := watchColor(c, info2)
286 if err := c.Roundtrip(); err != nil {
287 t.Fatal(err)
288 }
289 if len(hdr.values) < 8 || hdr.values[1][0] != bt2020 || hdr.values[3][0] != 50 || hdr.values[5][0] != 50 || hdr.values[5][1] != 1000 || hdr.values[6][0] != 1000 || hdr.values[7][0] != 400 {
290 t.Fatalf("HDR info %v %v", hdr.events, hdr.values)
291 }
292}
293func TestColorParametricCombinations(t *testing.T) {
294 for _, tc := range []struct {
295 name string
296 prim, tf uint32
297 ready bool
298 }{
299 {"sRGB gamma22", srgb, gamma22, false},
300 {"sRGB sRGB", srgb, uint32(cm.WpColorManagerV1TransferFunctionSrgb), true},
301 {"BT2020 PQ", bt2020, pq, true},
302 {"sRGB extended linear", srgb, extLinear, true},
303 {"BT2020 extended linear", bt2020, extLinear, false},
304 {"sRGB PQ", srgb, pq, false},
305 {"BT2020 gamma22", bt2020, gamma22, false},
306 } {
307 t.Run(tc.name, func(t *testing.T) {
308 _, c, _ := colorServer(t)
309 manager := bindVersion(t, c, "wp_color_manager_v1", 2)
310 registerProtocol(t, c, manager)
311 creator := c.AllocateID()
312 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestCreateParametricCreator, creator)
313 registerProtocol(t, c, creator)
314 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetPrimariesNamed, tc.prim)
315 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetTfNamed, tc.tf)
316 image := c.AllocateID()
317 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestCreate, image)
318 ev := watchColor(c, image)
319 if err := c.Roundtrip(); err != nil {
320 t.Fatal(err)
321 }
322 if len(ev.events) != 1 {
323 t.Fatalf("events %v", ev.events)
324 }
325 if tc.ready {
326 if uint32(ev.events[0]) != cm.WpImageDescriptionV1EventReady2 {
327 t.Fatalf("events %v", ev.events)
328 }
329 } else if uint32(ev.events[0]) != cm.WpImageDescriptionV1EventFailed || ev.values[0][0] != uint32(cm.WpImageDescriptionV1CauseUnsupported) {
330 t.Fatalf("failure events %v values %v", ev.events, ev.values)
331 }
332 })
333 }
334}
335
336func TestColorProtocolErrors(t *testing.T) {
337 for _, tc := range []struct {
338 name string
339 op uint32
340 args []any
341 code uint32
342 complete bool
343 }{
344 {"incomplete", cm.WpImageDescriptionCreatorParamsV1RequestCreate, []any{uint32(90)}, 0, false},
345 {"invalid tf", cm.WpImageDescriptionCreatorParamsV1RequestSetTfNamed, []any{uint32(99)}, 3, false},
346 {"invalid primaries", cm.WpImageDescriptionCreatorParamsV1RequestSetPrimariesNamed, []any{uint32(99)}, 4, false},
347 {"unsupported power", cm.WpImageDescriptionCreatorParamsV1RequestSetTfPower, []any{uint32(22000)}, 2, false},
348 {"unsupported primaries", cm.WpImageDescriptionCreatorParamsV1RequestSetPrimaries, []any{int32(1), int32(1), int32(1), int32(1), int32(1), int32(1), int32(1), int32(1)}, 2, false},
349 {"unsupported luminances", cm.WpImageDescriptionCreatorParamsV1RequestSetLuminances, []any{uint32(1), uint32(100), uint32(100)}, 2, false},
350 {"unsupported mastering primaries", cm.WpImageDescriptionCreatorParamsV1RequestSetMasteringDisplayPrimaries, []any{int32(1), int32(1), int32(1), int32(1), int32(1), int32(1), int32(1), int32(1)}, 2, false},
351 {"unsupported mastering luminance", cm.WpImageDescriptionCreatorParamsV1RequestSetMasteringLuminance, []any{uint32(1), uint32(100)}, 2, false},
352 {"already tf", cm.WpImageDescriptionCreatorParamsV1RequestSetTfNamed, []any{pq}, 1, true},
353 {"already primaries", cm.WpImageDescriptionCreatorParamsV1RequestSetPrimariesNamed, []any{bt2020}, 1, true},
354 {"max fall", cm.WpImageDescriptionCreatorParamsV1RequestCreate, []any{uint32(90)}, 5, true},
355 } {
356 t.Run(tc.name, func(t *testing.T) {
357 _, c, _ := colorServer(t)
358 manager := bindVersion(t, c, "wp_color_manager_v1", 2)
359 registerProtocol(t, c, manager)
360 creator := c.AllocateID()
361 requestProtocol(t, c, manager, cm.WpColorManagerV1RequestCreateParametricCreator, creator)
362 registerProtocol(t, c, creator)
363 if tc.complete {
364 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetTfNamed, pq)
365 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetPrimariesNamed, bt2020)
366 if tc.name == "max fall" {
367 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetMaxCll, uint32(100))
368 requestProtocol(t, c, creator, cm.WpImageDescriptionCreatorParamsV1RequestSetMaxFall, uint32(200))
369 }
370 }
371 args := tc.args
372 if tc.op == cm.WpImageDescriptionCreatorParamsV1RequestCreate {
373 args = []any{c.AllocateID()}
374 }
375 requestProtocol(t, c, creator, tc.op, args...)
376 expectProtocolError(t, c, creator, tc.code)
377 })
378 }
379}