1// Command testpattern is a minimal Wayland client that shows a grid of known
2// colours, to check by eye or by screenshot that NeferWL renders them
3// correctly:
4//
5// WAYLAND_DISPLAY=wayland-1 go run ./examples/testpattern -mode hdr -fullscreen
6//
7// Modes:
8//
9// sdr XRGB8888 wl_shm buffer of sRGB patches, no colour management
10// hdr XR30 linear dmabuf of BT.2020 PQ patches, declared with
11// wp_color_manager_v1 (st2084_pq, bt2020); needs /dev/udmabuf
12//
13// The patches are defined in the pattern sub-package. The client prints
14// "ready <w> <h>" on stderr once the window is mapped and drawn at its final
15// size, and, in hdr mode, "output-tf <n>" first: the named transfer function
16// of the compositor's output (11 is PQ, so virtual or real HDR is on; 0 when
17// the output does not report one).
18//
19// With -fullscreen the window asks for fullscreen and "ready" waits for the
20// compositor to grant it. NeferWL drops set_fullscreen sent before the
21// surface is mapped and ignores client fullscreen requests during the first
22// second after the map (core's fullscreenGrace), so the client maps first and
23// sends the request 1.2 s after its first buffer commit.
24//
25// The client runs until the display closes, then exits 0.
26package main
27
28import (
29 "errors"
30 "flag"
31 "fmt"
32 "io"
33 "math"
34 "net"
35 "os"
36 "syscall"
37 "time"
38
39 "github.com/bnema/neferwl/examples/testpattern/pattern"
40 "github.com/bnema/wlturbo"
41 "github.com/bnema/wlturbo/protocol/colormanagement"
42 clientcore "github.com/bnema/wlturbo/protocol/core"
43 "github.com/bnema/wlturbo/protocol/linuxdmabuf"
44 "github.com/bnema/wlturbo/protocol/xdgshell"
45 "golang.org/x/sys/unix"
46)
47
48const (
49 defaultWidth, defaultHeight = 640, 360
50 // fullscreenDelay outlasts core's 1 s fullscreenGrace.
51 fullscreenDelay = 1200 * time.Millisecond
52
53 shmXRGB8888 = 1
54 fourccXR30 = 'X' | 'R'<<8 | '3'<<16 | '0'<<24
55)
56
57func main() {
58 mode := flag.String("mode", "sdr", "patch set: sdr or hdr")
59 fullscreen := flag.Bool("fullscreen", false, "ask for fullscreen after mapping")
60 flag.Parse()
61 if *mode != "sdr" && *mode != "hdr" {
62 fmt.Fprintf(os.Stderr, "testpattern: unknown mode %q (want sdr or hdr)\n", *mode)
63 os.Exit(2)
64 }
65 if err := run(*mode, *fullscreen); err != nil {
66 fmt.Fprintln(os.Stderr, "testpattern:", err)
67 os.Exit(1)
68 }
69}
70
71// client holds the objects and state of one window. Only the dispatch
72// goroutine touches the state; the fullscreen timer only sends a request.
73type client struct {
74 d *wlturbo.Display
75 mode string
76 fullscreen bool
77
78 surface *clientcore.Surface
79 top *xdgshell.XdgToplevel
80 shm *clientcore.Shm
81 dmabuf *linuxdmabuf.LinuxDmabuf
82
83 // pending toplevel configure, applied by the xdg_surface configure.
84 pendW, pendH int32
85 pendFull bool
86
87 w, h int
88 buffer *clientcore.Buffer
89 ready bool
90 closed bool
91 err error
92 xr30Lin bool // XR30 with modifier 0 advertised
93 timerSet bool
94}
95
96func run(mode string, fullscreen bool) error {
97 d, err := wlturbo.Connect("")
98 if err != nil {
99 return err
100 }
101 defer d.Close()
102 if err := d.Roundtrip(); err != nil {
103 return err
104 }
105 c := &client{d: d, mode: mode, fullscreen: fullscreen}
106 bind := func(iface string, version uint32, proxy wlturbo.Proxy) error {
107 _, err := d.Registry().BindNegotiated(iface, version, proxy)
108 return err
109 }
110 compositor := clientcore.NewCompositor(d.Context())
111 if err := bind(clientcore.CompositorInterface, 4, compositor); err != nil {
112 return err
113 }
114 wm := xdgshell.NewXdgWmBase(d.Context())
115 if err := bind(xdgshell.XdgWmBaseInterface, 1, wm); err != nil {
116 return err
117 }
118 wm.OnPing(func(serial uint32) { _ = wm.Pong(serial) })
119
120 var colour *colormanagement.WpColorManager
121 var desc *colormanagement.WpImageDescription
122 if mode == "sdr" {
123 c.shm = clientcore.NewShm(d.Context())
124 if err := bind(clientcore.ShmInterface, 1, c.shm); err != nil {
125 return err
126 }
127 c.shm.OnFormat(func(uint32) {})
128 } else {
129 c.dmabuf = linuxdmabuf.NewLinuxDmabuf(d.Context())
130 if v, err := d.Registry().BindNegotiated(linuxdmabuf.LinuxDmabufInterface, 3, c.dmabuf); err != nil {
131 return err
132 } else if v < 3 {
133 return errors.New("linear XR30 unsupported: zwp_linux_dmabuf_v1 older than version 3")
134 }
135 c.dmabuf.OnFormat(func(uint32) {})
136 c.dmabuf.OnModifier(func(format, hi, lo uint32) {
137 if format == fourccXR30 && hi == 0 && lo == 0 {
138 c.xr30Lin = true
139 }
140 })
141 colour = colormanagement.NewWpColorManager(d.Context())
142 if err := bind(colormanagement.WpColorManagerInterface, 2, colour); err != nil {
143 return err
144 }
145 if err := d.Roundtrip(); err != nil {
146 return err
147 }
148 if !c.xr30Lin {
149 return errors.New("linear XR30 unsupported")
150 }
151 if desc, err = c.describe(colour); err != nil {
152 return err
153 }
154 if err := c.printOutputTF(colour); err != nil {
155 return err
156 }
157 }
158
159 if c.surface, err = compositor.CreateSurface(); err != nil {
160 return err
161 }
162 if desc != nil {
163 cs, err := colour.GetSurface(c.surface)
164 if err != nil {
165 return err
166 }
167 if err := cs.SetImageDescription(desc, 0); err != nil {
168 return err
169 }
170 }
171 xs, err := wm.GetXdgSurface(c.surface)
172 if err != nil {
173 return err
174 }
175 if c.top, err = xs.GetToplevel(); err != nil {
176 return err
177 }
178 _ = c.top.SetTitle("testpattern")
179 _ = c.top.SetAppId("testpattern")
180 c.top.OnConfigure(func(w, h int32, states []byte) {
181 c.pendW, c.pendH, c.pendFull = w, h, hasState(states, xdgshell.STATE_FULLSCREEN)
182 })
183 c.top.OnClose(func() { c.closed = true })
184 xs.OnConfigure(func(serial uint32) {
185 if err := xs.AckConfigure(serial); err != nil {
186 c.err = err
187 return
188 }
189 if err := c.configured(); err != nil {
190 c.err = err
191 }
192 })
193 if err := c.surface.Commit(); err != nil { // initial commit asks for the first configure
194 return err
195 }
196 for !c.closed && c.err == nil {
197 if err := d.Dispatch(); err != nil {
198 if isClosed(err) {
199 return nil
200 }
201 return err
202 }
203 }
204 return c.err
205}
206
207// describe creates the PQ/BT.2020 image description and waits until it is
208// ready: a surface may only use a ready description.
209func (c *client) describe(colour *colormanagement.WpColorManager) (*colormanagement.WpImageDescription, error) {
210 creator, err := colour.CreateParametricCreator()
211 if err != nil {
212 return nil, err
213 }
214 if err := creator.SetTfNamed(colormanagement.TRANSFER_FUNCTION_ST2084_PQ); err != nil {
215 return nil, err
216 }
217 if err := creator.SetPrimariesNamed(colormanagement.PRIMARIES_BT2020); err != nil {
218 return nil, err
219 }
220 desc, err := creator.Create()
221 if err != nil {
222 return nil, err
223 }
224 ready, failed := false, ""
225 desc.OnReady2(func(uint32, uint32) { ready = true })
226 desc.OnFailed(func(cause uint32, msg string) { failed = fmt.Sprintf("cause %d: %s", cause, msg) })
227 if err := c.until(func() bool { return ready || failed != "" }); err != nil {
228 return nil, err
229 }
230 if failed != "" {
231 return nil, fmt.Errorf("image description failed: %s", failed)
232 }
233 return desc, nil
234}
235
236// printOutputTF reports the named transfer function of the first output on
237// stderr as "output-tf <n>" (0 when it reports none).
238func (c *client) printOutputTF(colour *colormanagement.WpColorManager) error {
239 g, ok := c.d.Registry().FindGlobal(clientcore.OutputInterface)
240 if !ok {
241 return errors.New("compositor has no wl_output")
242 }
243 output := clientcore.NewOutput(c.d.Context())
244 if err := c.d.Registry().Bind(g.Name, g.Interface, min(g.Version, 4), output); err != nil {
245 return err
246 }
247 co, err := colour.GetOutput(output)
248 if err != nil {
249 return err
250 }
251 desc, err := co.GetImageDescription()
252 if err != nil {
253 return err
254 }
255 ready, failed := false, false
256 desc.OnReady2(func(uint32, uint32) { ready = true })
257 desc.OnFailed(func(uint32, string) { failed = true })
258 if err := c.until(func() bool { return ready || failed }); err != nil {
259 return err
260 }
261 if failed {
262 return errors.New("output image description failed")
263 }
264 info, err := desc.GetInformation()
265 if err != nil {
266 return err
267 }
268 tf, done := uint32(0), false
269 info.OnTfNamed(func(v uint32) { tf = v })
270 info.OnDone(func() { done = true })
271 if err := c.until(func() bool { return done }); err != nil {
272 return err
273 }
274 fmt.Fprintf(os.Stderr, "output-tf %d\n", tf)
275 return nil
276}
277
278// until roundtrips until cond holds; setup events arrive within a few.
279func (c *client) until(cond func() bool) error {
280 for range 20 {
281 if cond() {
282 return nil
283 }
284 if err := c.d.Roundtrip(); err != nil {
285 return err
286 }
287 }
288 if cond() {
289 return nil
290 }
291 return errors.New("compositor did not answer")
292}
293
294// configured applies a toplevel configure after it was acknowledged: it
295// (re)builds the buffer when the size changed, commits, and reports ready.
296func (c *client) configured() error {
297 w, h := int(c.pendW), int(c.pendH)
298 if w <= 0 || h <= 0 {
299 w, h = c.w, c.h // the client picks the size
300 if w == 0 || h == 0 {
301 w, h = defaultWidth, defaultHeight
302 }
303 }
304 if c.buffer == nil || w != c.w || h != c.h {
305 buf, err := c.newBuffer(w, h)
306 if err != nil {
307 return err
308 }
309 old := c.buffer
310 c.buffer, c.w, c.h = buf, w, h
311 defer func() {
312 if old != nil {
313 _ = old.Destroy()
314 }
315 }()
316 }
317 if err := c.surface.Attach(c.buffer, 0, 0); err != nil {
318 return err
319 }
320 if err := c.surface.Damage(0, 0, int32(c.w), int32(c.h)); err != nil {
321 return err
322 }
323 if err := c.surface.Commit(); err != nil {
324 return err
325 }
326 if c.fullscreen && !c.timerSet {
327 c.timerSet = true
328 top := c.top
329 time.AfterFunc(fullscreenDelay, func() {
330 if err := top.SetFullscreen(nil); err != nil {
331 fmt.Fprintf(os.Stderr, "testpattern: set_fullscreen: %v\n", err)
332 }
333 })
334 }
335 if !c.ready && (!c.fullscreen || c.pendFull) {
336 c.ready = true
337 fmt.Fprintf(os.Stderr, "ready %d %d\n", c.w, c.h)
338 }
339 return nil
340}
341
342func (c *client) newBuffer(w, h int) (*clientcore.Buffer, error) {
343 patches := pattern.Patches(c.mode, w, h)
344 if c.mode == "sdr" {
345 return c.shmBuffer(w, h, patches)
346 }
347 return c.dmabufBuffer(w, h, patches)
348}
349
350// shmBuffer draws the sRGB patches into XRGB8888 (little-endian B, G, R, X).
351func (c *client) shmBuffer(w, h int, patches []pattern.Patch) (*clientcore.Buffer, error) {
352 size := w * h * 4
353 fd, err := wlturbo.CreateAnonymousFile(int64(size))
354 if err != nil {
355 return nil, err
356 }
357 data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
358 if err != nil {
359 _ = unix.Close(fd)
360 return nil, err
361 }
362 for _, p := range patches {
363 for y := p.Rect.Min.Y; y < p.Rect.Max.Y; y++ {
364 for x := p.Rect.Min.X; x < p.Rect.Max.X; x++ {
365 i := (y*w + x) * 4
366 data[i], data[i+1], data[i+2], data[i+3] = p.SRGB[2], p.SRGB[1], p.SRGB[0], 0xff
367 }
368 }
369 }
370 // The compositor maps the file itself; ours is not needed any more.
371 _ = unix.Munmap(data)
372 pool, err := c.shm.CreatePool(fd, int32(size)) // closes fd once sent
373 if err != nil {
374 _ = unix.Close(fd)
375 return nil, err
376 }
377 buf, err := pool.CreateBuffer(0, int32(w), int32(h), int32(w*4), shmXRGB8888)
378 _ = pool.Destroy()
379 return buf, err
380}
381
382// dmabufBuffer draws the PQ patches into a udmabuf as XR30 words
383// b | g<<10 | r<<20.
384func (c *client) dmabufBuffer(w, h int, patches []pattern.Patch) (*clientcore.Buffer, error) {
385 code := func(nits float64) uint32 { return uint32(math.Round(pattern.PQEncode(nits) * 1023)) }
386 f, err := udmabuf(w*h*4, func(data []byte) {
387 for _, p := range patches {
388 word := code(p.Nits[2]) | code(p.Nits[1])<<10 | code(p.Nits[0])<<20
389 for y := p.Rect.Min.Y; y < p.Rect.Max.Y; y++ {
390 for x := p.Rect.Min.X; x < p.Rect.Max.X; x++ {
391 i := (y*w + x) * 4
392 data[i], data[i+1], data[i+2], data[i+3] = byte(word), byte(word>>8), byte(word>>16), byte(word>>24)
393 }
394 }
395 }
396 })
397 if err != nil {
398 return nil, err
399 }
400 defer f.Close()
401 params, err := c.dmabuf.CreateParams()
402 if err != nil {
403 return nil, err
404 }
405 // Add sends a duplicate: the send closes the descriptor it is given.
406 fd, err := unix.Dup(int(f.Fd()))
407 if err != nil {
408 return nil, err
409 }
410 if err := params.Add(fd, 0, 0, uint32(w*4), 0, 0); err != nil {
411 _ = unix.Close(fd)
412 return nil, err
413 }
414 buf, err := params.CreateImmed(int32(w), int32(h), fourccXR30, 0)
415 _ = params.Destroy()
416 return buf, err
417}
418
419func hasState(states []byte, want uint32) bool {
420 for i := 0; i+4 <= len(states); i += 4 {
421 if uint32(states[i])|uint32(states[i+1])<<8|uint32(states[i+2])<<16|uint32(states[i+3])<<24 == want {
422 return true
423 }
424 }
425 return false
426}
427
428// isClosed reports whether err is the compositor going away.
429func isClosed(err error) bool {
430 return errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, os.ErrClosed) ||
431 errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) || errors.Is(err, net.ErrClosed)
432}