v0.10.0 d5d679d

main.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close