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}