v0.1.0 47aaa0f

window.go RawHistoryFolder

1//go:build linux
2
3// Package wayland owns the minimal xdg-shell window and presentation protocol
4// objects. Vulkan allocations and DMA-BUF image creation remain the renderer's
5// responsibility; a window may not attach until the first configure is acked.
6package wayland
7
8import (
9	"encoding/binary"
10	"errors"
11	"fmt"
12	"math"
13	"sync"
14	"sync/atomic"
15
16	"github.com/bnema/wlturbo"
17	"github.com/bnema/wlturbo/protocol/core"
18	"github.com/bnema/wlturbo/protocol/cursorshape"
19	"github.com/bnema/wlturbo/protocol/drmsyncobj"
20	"github.com/bnema/wlturbo/protocol/fractionalscale"
21	"github.com/bnema/wlturbo/protocol/linuxdmabuf"
22	"github.com/bnema/wlturbo/protocol/viewporter"
23	"github.com/bnema/wlturbo/protocol/xdgshell"
24	"github.com/bnema/wlturbo/wl"
25)
26
27// CapabilityError reports a missing or insufficient required protocol.
28type CapabilityError struct {
29	Name  string
30	Cause error
31}
32
33func (e *CapabilityError) Error() string {
34	return fmt.Sprintf("Wayland capability %s: %v", e.Name, e.Cause)
35}
36func (e *CapabilityError) Unwrap() error { return e.Cause }
37
38const (
39	DRMXRGB8888 = uint32(0x34325258)
40	DRMARGB8888 = uint32(0x34325241)
41)
42
43type Window struct {
44	Display                   *wl.Display
45	Compositor                *core.Compositor
46	Shell                     *xdgshell.XdgWmBase
47	Dmabuf                    *linuxdmabuf.LinuxDmabuf
48	SyncManager               *drmsyncobj.WpLinuxDrmSyncobjManager
49	Surface                   *core.Surface
50	XdgSurface                *xdgshell.XdgSurface
51	Toplevel                  *xdgshell.XdgToplevel
52	SyncSurface               *drmsyncobj.WpLinuxDrmSyncobjSurface
53	Viewporter                *viewporter.WpViewporter
54	Viewport                  *viewporter.WpViewport
55	ScaleManager              *fractionalscale.WpFractionalScaleManager
56	Fractional                *fractionalscale.WpFractionalScale
57	Feedback                  *linuxdmabuf.LinuxDmabufFeedback
58	Seat                      *core.Seat
59	Pointer                   *core.Pointer
60	CursorShapes              *cursorshape.WpCursorShapeManager // optional
61	cursor                    cursorState
62	pointerGen                uint64 // bumped per wl_pointer; session loop only
63	WLKeyboard                *core.Keyboard
64	seatName, seatVersion     uint32
65	Width, Height             int32
66	Scale                     float64
67	Configured, Closed, Dirty bool
68	Transparent               bool
69	FrameReady                bool
70	// Feedback fields are populated by the default feedback event stream.
71	MainDevice    uint64
72	Formats       []linuxdmabuf.FormatEntry
73	Tranches      [][]linuxdmabuf.FormatEntry
74	feedbackErr   error
75	events        chan Event
76	readerDone    chan struct{}
77	readerStop    chan struct{}
78	readerOnce    sync.Once
79	readerStarted atomic.Bool
80	inputAdapter  *InputAdapter
81	inputQueue    []InputEvent
82	// InputOverflow counts events discarded when the 4096-event queue fills.
83	InputOverflow uint64
84}
85
86func Connect(name string, width, height int32, transparent bool) (_ *Window, err error) {
87	if width <= 0 || height <= 0 {
88		return nil, fmt.Errorf("invalid logical size %dx%d", width, height)
89	}
90	d, err := wl.Connect(name)
91	if err != nil {
92		return nil, err
93	}
94	w := &Window{events: make(chan Event, 128), readerStop: make(chan struct{}), Display: d, Width: width, Height: height, Scale: 1, Transparent: transparent}
95	defer func() {
96		if err != nil {
97			_ = w.Close()
98		}
99	}()
100	if err = d.Roundtrip(); err != nil {
101		return nil, err
102	}
103	ctx := d.Context()
104	bind := func(name string, supported, minimum uint32, proxy wl.Proxy) error {
105		advertised, ok := d.Registry().FindGlobal(name)
106		if !ok || advertised.Version < minimum {
107			return &CapabilityError{Name: name, Cause: fmt.Errorf("requires version %d, announced %d", minimum, advertised.Version)}
108		}
109		got, e := d.Registry().BindNegotiated(name, supported, proxy)
110		if e != nil {
111			return &CapabilityError{Name: name, Cause: e}
112		}
113		if got < minimum {
114			return &CapabilityError{Name: name, Cause: fmt.Errorf("requires version %d, compositor negotiated %d", minimum, got)}
115		}
116		return nil
117	}
118	w.Compositor = core.NewCompositor(ctx)
119	if err = bind(core.CompositorInterface, 6, 4, w.Compositor); err != nil {
120		return nil, err
121	}
122	w.Shell = xdgshell.NewXdgWmBase(ctx)
123	if err = bind(xdgshell.XdgWmBaseInterface, 6, 1, w.Shell); err != nil {
124		return nil, err
125	}
126	w.Dmabuf = linuxdmabuf.NewLinuxDmabuf(ctx)
127	if err = bind(linuxdmabuf.LinuxDmabufInterface, 4, 4, w.Dmabuf); err != nil {
128		return nil, err
129	}
130	w.SyncManager = drmsyncobj.NewWpLinuxDrmSyncobjManager(ctx)
131	if err = bind(drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, 1, w.SyncManager); err != nil {
132		return nil, err
133	}
134	w.Viewporter = viewporter.NewWpViewporter(ctx)
135	if _, e := d.Registry().BindNegotiated(viewporter.WpViewporterInterface, 1, w.Viewporter); e != nil {
136		if !errors.Is(e, wlturbo.ErrGlobalNotFound) {
137			return nil, e
138		}
139		ctx.Unregister(w.Viewporter)
140		w.Viewporter = nil
141	}
142	w.ScaleManager = fractionalscale.NewWpFractionalScaleManager(ctx)
143	if _, e := d.Registry().BindNegotiated(fractionalscale.WpFractionalScaleManagerInterface, 1, w.ScaleManager); e != nil {
144		if !errors.Is(e, wlturbo.ErrGlobalNotFound) {
145			return nil, e
146		}
147		ctx.Unregister(w.ScaleManager)
148		w.ScaleManager = nil
149	}
150	w.CursorShapes = cursorshape.NewWpCursorShapeManager(ctx)
151	if _, e := d.Registry().BindNegotiated(cursorshape.WpCursorShapeManagerInterface, 1, w.CursorShapes); e != nil {
152		if !errors.Is(e, wlturbo.ErrGlobalNotFound) {
153			return nil, e
154		}
155		ctx.Unregister(w.CursorShapes)
156		w.CursorShapes = nil
157	}
158	if err = w.BindSeat(); err != nil {
159		return nil, err
160	}
161	w.Shell.OnPing(func(serial uint32) {
162		if e := w.Shell.Pong(serial); e != nil {
163			w.feedbackFailure(e)
164		}
165	})
166	w.Surface, err = w.Compositor.CreateSurface()
167	if err != nil {
168		return nil, err
169	}
170	if w.Viewporter != nil {
171		w.Viewport, err = w.Viewporter.GetViewport(w.Surface)
172		if err != nil {
173			return nil, err
174		}
175	}
176	if w.ScaleManager != nil {
177		w.Fractional, err = w.ScaleManager.GetFractionalScale(w.Surface)
178		if err != nil {
179			return nil, err
180		}
181		w.Fractional.OnPreferredScale(func(scale uint32) {
182			if w.readerStarted.Load() {
183				w.post(Event{Kind: PreferredScale, Scale: scale})
184				return
185			}
186			if scale == 0 {
187				w.feedbackFailure(fmt.Errorf("invalid fractional scale zero"))
188				return
189			}
190			if scale%120 != 0 && w.Viewport == nil {
191				w.feedbackFailure(&CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %d/120 requires viewporter", scale)})
192				return
193			}
194			if w.Scale != float64(scale)/120 {
195				w.Scale = float64(scale) / 120
196				w.Dirty = true
197			}
198		})
199	}
200	w.Surface.OnPreferredBufferScale(func(factor int32) {
201		if w.readerStarted.Load() {
202			w.post(Event{Kind: BufferScale, Factor: factor})
203			return
204		}
205		if w.Fractional == nil && factor > 0 && w.Scale != float64(factor) {
206			w.Scale = float64(factor)
207			w.Dirty = true
208		}
209	})
210	w.XdgSurface, err = w.Shell.GetXdgSurface(w.Surface)
211	if err != nil {
212		return nil, err
213	}
214	w.Toplevel, err = w.XdgSurface.GetToplevel()
215	if err != nil {
216		return nil, err
217	}
218	w.Toplevel.OnClose(func() {
219		if w.readerStarted.Load() {
220			w.post(Event{Kind: CloseEvent})
221			return
222		}
223		w.Closed = true
224	})
225	w.Toplevel.OnConfigure(func(width, height int32, _ []byte) {
226		if w.readerStarted.Load() {
227			w.post(Event{Kind: ConfigureSize, Width: width, Height: height})
228			return
229		}
230		if width > 0 && height > 0 && (w.Width != width || w.Height != height) {
231			w.Width, w.Height, w.Dirty = width, height, true
232		}
233	})
234	w.XdgSurface.OnConfigure(func(serial uint32) {
235		if w.readerStarted.Load() {
236			w.post(Event{Kind: ConfigureSerial, Serial: serial})
237			return
238		}
239		if e := w.XdgSurface.AckConfigure(serial); e != nil {
240			w.feedbackFailure(e)
241			return
242		}
243		w.Configured, w.FrameReady = true, true
244	})
245	w.SyncSurface, err = w.SyncManager.GetSurface(w.Surface)
246	if err != nil {
247		return nil, err
248	}
249	w.Feedback, err = w.Dmabuf.GetDefaultFeedback()
250	if err != nil {
251		return nil, err
252	}
253	w.watchFeedback()
254	if err = w.Surface.Commit(); err != nil {
255		return nil, err
256	} // no buffer before configure
257	for !w.Configured || w.MainDevice == 0 || len(w.Tranches) == 0 {
258		if w.Closed {
259			return nil, fmt.Errorf("window closed before initial configure and feedback")
260		}
261		if err = w.Dispatch(); err != nil {
262			return nil, err
263		}
264	}
265	return w, nil
266}
267
268func (w *Window) Dispatch() error {
269	if w.feedbackErr != nil {
270		return w.feedbackErr
271	}
272	if err := w.Display.Dispatch(); err != nil {
273		return err
274	}
275	return w.feedbackErr
276}
277
278func (w *Window) PhysicalSize() (int32, int32, error) {
279	if w.Scale <= 0 || w.Width <= 0 || w.Height <= 0 {
280		return 0, 0, fmt.Errorf("invalid window geometry")
281	}
282	if w.Scale != math.Trunc(w.Scale) && w.Viewport == nil {
283		return 0, 0, &CapabilityError{Name: viewporter.WpViewporterInterface, Cause: fmt.Errorf("fractional scale %g requires viewporter", w.Scale)}
284	}
285	// Ignore sub-nanopixel floating noise at exact integer boundaries.
286	x, y := math.Ceil(float64(w.Width)*w.Scale-1e-9), math.Ceil(float64(w.Height)*w.Scale-1e-9)
287	if x > math.MaxInt32 || y > math.MaxInt32 {
288		return 0, 0, fmt.Errorf("buffer dimensions overflow")
289	}
290	return int32(x), int32(y), nil
291}
292
293// Present sets both syncobj points on every buffer commit. The caller supplies
294// an already-imported wl_buffer and two persistent Wayland timeline objects.
295func (w *Window) Present(buffer *core.Buffer, acquire, release *drmsyncobj.WpLinuxDrmSyncobjTimeline, acquirePoint, releasePoint uint64) error {
296	if !w.Configured || buffer == nil || acquire == nil || release == nil {
297		return fmt.Errorf("unconfigured surface or missing buffer/timeline")
298	}
299	if !w.FrameReady {
300		return fmt.Errorf("frame callback pending")
301	}
302	pw, ph, err := w.PhysicalSize()
303	if err != nil {
304		return err
305	}
306	if w.Viewport != nil {
307		if err = w.Viewport.SetDestination(w.Width, w.Height); err != nil {
308			return err
309		}
310	} else if w.Scale > 1 {
311		if err = w.Surface.SetBufferScale(int32(w.Scale)); err != nil {
312			return err
313		}
314	}
315	if w.Transparent {
316		if err = w.Surface.SetOpaqueRegion(nil); err != nil {
317			return err
318		}
319	} else {
320		region, e := w.Compositor.CreateRegion()
321		if e != nil {
322			return e
323		}
324		if err = region.Add(0, 0, w.Width, w.Height); err == nil {
325			err = w.Surface.SetOpaqueRegion(region)
326		}
327		_ = region.Destroy()
328		if err != nil {
329			return err
330		}
331	}
332	if err = w.SyncSurface.SetAcquirePoint(acquire, uint32(acquirePoint>>32), uint32(acquirePoint)); err != nil {
333		return err
334	}
335	if err = w.SyncSurface.SetReleasePoint(release, uint32(releasePoint>>32), uint32(releasePoint)); err != nil {
336		return err
337	}
338	if err = w.Surface.Attach(buffer, 0, 0); err != nil {
339		return err
340	}
341	if err = w.Surface.DamageBuffer(0, 0, pw, ph); err != nil {
342		return err
343	}
344	cb, e := w.Surface.Frame()
345	if e != nil {
346		return e
347	}
348	cb.OnDone(func(_ uint32) {
349		if w.readerStarted.Load() {
350			w.post(Event{Kind: FrameEvent})
351			return
352		}
353		w.FrameReady = true
354	})
355	if err = w.Surface.Commit(); err != nil {
356		return err
357	}
358	w.FrameReady, w.Dirty = false, false
359	return nil
360}
361
362// DestroySyncSurface stops associating new explicit-sync points with the surface.
363// The session calls it after committing a null attachment at shutdown.
364func (w *Window) DestroySyncSurface() error {
365	if w == nil || w.SyncSurface == nil {
366		return nil
367	}
368	err := w.SyncSurface.Destroy()
369	if err == nil {
370		w.SyncSurface = nil
371	}
372	return err
373}
374
375// Close destroys children before their parents. The caller first detaches and
376// waits for its own GPU work before destroying its Vulkan allocations.
377func (w *Window) Close() error {
378	if w == nil {
379		return nil
380	}
381	// The loop sends protocol destructors before Close; the reader is
382	// unblocked by the connection close at the end of this method.
383	if w.inputAdapter != nil {
384		w.inputAdapter.Close()
385		w.inputAdapter = nil
386	}
387	if w.Seat != nil {
388		_ = w.releaseSeat()
389	}
390	if w.CursorShapes != nil {
391		_ = w.CursorShapes.Destroy()
392		w.CursorShapes = nil
393	}
394	if w.Feedback != nil {
395		_ = w.Feedback.Destroy()
396		w.Feedback = nil
397	}
398	if w.SyncSurface != nil {
399		_ = w.SyncSurface.Destroy()
400		w.SyncSurface = nil
401	}
402	if w.Toplevel != nil {
403		_ = w.Toplevel.Destroy()
404		w.Toplevel = nil
405	}
406	if w.XdgSurface != nil {
407		_ = w.XdgSurface.Destroy()
408		w.XdgSurface = nil
409	}
410	if w.Fractional != nil {
411		_ = w.Fractional.Destroy()
412		w.Fractional = nil
413	}
414	if w.Viewport != nil {
415		_ = w.Viewport.Destroy()
416		w.Viewport = nil
417	}
418	if w.Surface != nil {
419		_ = w.Surface.Destroy()
420		w.Surface = nil
421	}
422	if w.SyncManager != nil {
423		_ = w.SyncManager.Destroy()
424		w.SyncManager = nil
425	}
426	if w.Dmabuf != nil {
427		_ = w.Dmabuf.Destroy()
428		w.Dmabuf = nil
429	}
430	if w.ScaleManager != nil {
431		_ = w.ScaleManager.Destroy()
432		w.ScaleManager = nil
433	}
434	if w.Viewporter != nil {
435		_ = w.Viewporter.Destroy()
436		w.Viewporter = nil
437	}
438	if w.Shell != nil {
439		_ = w.Shell.Destroy()
440		w.Shell = nil
441	}
442	// wl_compositor is bound at most at version 6; its release destructor
443	// needs version 7, so the proxy is only forgotten locally.
444	if w.Compositor != nil {
445		if c := w.Compositor.Context(); c != nil {
446			c.Unregister(w.Compositor)
447		}
448		w.Compositor = nil
449	}
450	if w.Display != nil {
451		if w.readerDone != nil {
452			close(w.readerStop)
453		}
454		err := w.Display.Close()
455		if w.readerDone != nil {
456			<-w.readerDone
457			w.readerDone = nil
458		}
459		for len(w.events) > 0 {
460			CloseEventFD(<-w.events)
461		}
462		w.Display = nil
463		return err
464	}
465	return nil
466}
467
468func (w *Window) watchFeedback() {
469	var table []linuxdmabuf.FormatEntry
470	var indices []uint16
471	w.Feedback.OnFormatTable(func(fd *wl.OwnedFD, size uint32) {
472		var err error
473		table, err = linuxdmabuf.ReadFormatTable(fd, size)
474		if err != nil {
475			w.feedbackFailure(err)
476		}
477	})
478	w.Feedback.OnMainDevice(func(device []byte) {
479		if len(device) != 8 {
480			w.feedbackFailure(fmt.Errorf("invalid dmabuf main_device length %d", len(device)))
481			return
482		}
483		if w.readerStarted.Load() {
484			w.post(Event{Kind: FeedbackMainDevice, Device: binary.NativeEndian.Uint64(device)})
485		} else {
486			w.MainDevice = binary.NativeEndian.Uint64(device)
487		}
488	})
489	w.Feedback.OnTrancheFormats(func(data []byte) {
490		var err error
491		indices, err = linuxdmabuf.TrancheIndices(data, len(table))
492		if err != nil {
493			w.feedbackFailure(err)
494		}
495	})
496	w.Feedback.OnTrancheDone(func() {
497		var tranche []linuxdmabuf.FormatEntry
498		for _, i := range indices {
499			tranche = append(tranche, table[i])
500		}
501		if w.readerStarted.Load() {
502			w.post(Event{Kind: FeedbackTranche, Tranche: tranche})
503		} else {
504			w.Tranches = append(w.Tranches, tranche)
505		}
506		indices = nil
507	})
508	w.Feedback.OnDone(func() {
509		if w.readerStarted.Load() {
510			w.post(Event{Kind: FeedbackFormats, Formats: table})
511		} else {
512			w.Formats = table
513		}
514	})
515}
516
517func (w *Window) feedbackFailure(err error) {
518	if w.readerStarted.Load() {
519		w.post(Event{Kind: FeedbackError, Err: err})
520	} else {
521		w.feedbackErr = err
522	}
523}

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 1

Tags 12

↑↓ moveenter openesc close