1package wayland
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "sync"
8 "sync/atomic"
9 "time"
10
11 "github.com/bnema/go-wayland-bindings/server/relativepointer"
12
13 "github.com/bnema/go-wayland-bindings/server/fractionalscale"
14 "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
15 "github.com/bnema/go-wayland-bindings/server/wayland"
16 "github.com/bnema/neferwl/internal/adapters/captureallow"
17 "github.com/bnema/neferwl/internal/adapters/clock"
18 "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock"
19 "github.com/bnema/neferwl/internal/adapters/workspaceid"
20 "github.com/bnema/neferwl/internal/ports"
21 "github.com/bnema/purego-libwayland/server"
22 "github.com/bnema/zerowrap"
23 "golang.org/x/sys/unix"
24)
25
26// Options configures the server. Outputs is the initial layout; core
27// replaces it with ports.SetOutputs.
28type Options struct {
29 RuntimeDir string
30 Outputs ports.Layout
31 // DMABuf is what the renderer imports; empty disables linux-dmabuf.
32 DMABuf ports.DMABufSupport
33 // SyncobjNode is the render node (/dev/dri/renderD*) timeline
34 // syncobjs are imported on; empty or without timeline support, no
35 // explicit sync is offered.
36 SyncobjNode string
37 Keymap string
38 RepeatRate, RepeatDelay int
39 // Clock runs idle notification timers; nil is the system clock.
40 Clock ports.Clock
41 // Security is display-owned protection control. Nil leaves session-lock
42 // unadvertised; independent protocol tests remain unlocked.
43 Security ports.SessionSecurityController
44 // WorkspaceIDs names workspaces (ext-workspace id); nil draws a fresh
45 // launch prefix. The state file must share the one of the session.
46 WorkspaceIDs *workspaceid.IDs
47 // CaptureAllow is the executable allowlist for screen capture (captureauth.go);
48 // nil lets every client capture, unless the session is protected. The
49 // caller keeps it current (captureallow.Store.Run).
50 CaptureAllow *captureallow.Store
51 // Casts receives the screencast events and inventory (screencast.go);
52 // nil serves no casts (Channels.CastCommands is not read).
53 Casts ports.CastSink
54 // LogDamage logs the damage of every fresh client commit at debug level
55 // (--debug=wayland-damage).
56 LogDamage bool
57 // syncDev replaces the render node's syncobj interface (tests).
58 syncDev syncobjDevice
59 // peer replaces the peer executable lookup (tests).
60 peer peerExe
61}
62
63// Channels carries client notifications and commands. Events may be unbuffered.
64type Channels struct {
65 // SecurityChanges requests ordered backend protection/release. The
66 // backend echoes actual inventory/proofs/barriers on SecurityEvents.
67 SecurityChanges chan<- ports.SecurityState
68 SecurityEvents <-chan ports.SecurityBackendEvent
69 LeaseRequests chan<- ports.LeaseMessage
70 LeaseEvents <-chan ports.LeaseMessage
71 Events chan<- ports.ClientEvent
72 Commands <-chan ports.ClientCommand
73 Workspaces <-chan ports.Workspaces
74 // Cursors receives the cursor the client under the pointer asks for.
75 Cursors chan<- ports.CursorChange
76 // Captures carries accepted capture requests, never blocking. Each
77 // accepted request holds one of ports.MaxCaptureInflight credits until its
78 // CaptureDone arrives on Captured, whether or not the client is still
79 // there; the backend must complete every request it takes. In production
80 // both channels have capacity >= ports.MaxCaptureInflight, so replies
81 // never block the backend; standalone tests may use smaller channels.
82 Captures chan<- ports.CaptureRequest
83 Captured <-chan ports.CaptureDone
84 // CastCommands carries the screencast adapter's commands (ports.CastCommand);
85 // they are applied in order on the display goroutine. Nil serves no casts.
86 CastCommands <-chan ports.CastCommand
87 // OutputFormats are the outputs' direct scanout formats, offered in
88 // dmabuf feedback to fullscreen surfaces.
89 OutputFormats <-chan ports.OutputFormats
90 OutputHeads <-chan ports.OutputHeads
91 OutputApply chan<- ports.OutputApply
92 OutputApplied <-chan ports.OutputApplied
93 // IdleInhibited reports whether D-Bus clients (org.freedesktop.ScreenSaver)
94 // inhibit idle; while true, it holds idle notifications like an inhibitor
95 // surface does.
96 IdleInhibited <-chan bool
97 // IdleActivity carries D-Bus SimulateUserActivity calls: idle
98 // notifications resume and restart as on input.
99 IdleActivity <-chan struct{}
100}
101
102type Server struct {
103 security ports.SessionSecurity
104 securityController ports.SessionSecurityController
105 securityPending []ports.SecurityState // display-owned ordered backend outbox
106 securityWake chan struct{}
107 sessionLock *sessionLock
108 lockReadiness sessionlock.Readiness
109 lockSurfaces map[ports.WindowID]*lockSurface
110 leaseDevices map[string]*leaseDevice
111 pendingLeases map[uint64]*leaseObject
112 pendingLeaseCards map[uint64]string
113 activeLeases map[leaseKey]*leaseObject
114 leaseMu sync.Mutex // only the outbound lease queue; never window state
115 leaseOut []ports.LeaseMessage
116 leaseReady chan struct{}
117 nextLeaseID uint64
118 nextCapture uint64
119 captureReplies map[uint64]func(ports.CaptureDone)
120 // capturedRing hands backend replies to the display goroutine
121 // (forwardCaptured); the display goroutine applies them in order.
122 capturedRing *postRing[ports.CaptureDone]
123 // captureInflight counts accepted captures until the backend completes
124 // them, independent of client resources; only the display loop touches it.
125 captureInflight map[uint64]struct{}
126 captureSources map[*server.Resource]*captureSource
127 captureSessions map[*captureSession]struct{}
128 // sessionsByID are the sessions registered with core (imagecopy.go);
129 // excl is the one live exclusion (exclusion.go); ownUID the compositor's.
130 sessionsByID map[uint64]*captureSession
131 // Screencast (screencast.go): casts by the adapter's ID and by the ID
132 // core knows them by; castRing carries the adapter's commands to the
133 // display goroutine; the castInv fields coalesce inventory publication.
134 casts ports.CastSink
135 castSessions map[uint64]*castSession
136 castByCore map[uint64]*castSession
137 castRing *postRing[ports.CastCommand]
138 castInvFn func()
139 castInvPosted bool
140 castInvSent bool
141 castInvLast ports.CastInventory
142 nextSession uint64
143 excl *exclusion
144 ownUID uint32
145 // captureAllow, peer and peers decide mayCapture (captureauth.go):
146 // peers keeps each client's pidfd until it is destroyed.
147 captureAllow *captureallow.Store
148 peer peerExe
149 peers map[server.Client]*peerID
150 // noPidfdWarned: the missing SO_PEERPIDFD is logged once.
151 noPidfdWarned bool
152 // sandboxed are the clients that connected through a security context
153 // (securitycontext.go); display goroutine only. securityStop is
154 // written once at shutdown to end the listeners, securityWG waits for
155 // them, securityLive counts them.
156 sandboxed map[server.Client]*sandbox
157 securityStop int
158 securityWG sync.WaitGroup
159 securityLive atomic.Int32
160 display *server.Display
161 env procEnv
162 slotsPending bool // core waits for a slot window
163 name string
164 cleanup func()
165 log zerowrap.Logger
166 logDamage bool
167 channels Channels
168 // awaiting holds frame callbacks by output name, due at its next
169 // frame (frameDue, or its page flip).
170 awaiting map[string][]*wayland.Callback
171 frameOwners map[*wayland.Callback]*surface
172 frameDue map[string]time.Time
173 // paceFD is the pace deadline timerfd, armed by ticks on the display
174 // goroutine (paceReady: registered with the display loop);
175 // paceDepth/paceDirty defer wakePacer inside a commit or tick (display
176 // goroutine only). paceSignal coalesces posted ticks (paceSignalFn).
177 paceFD int
178 paceReady bool
179 paceWarned bool // a pace timer failure was logged (once)
180 // paceArmed: the timerfd is armed for paceDeadline (display goroutine
181 // only); armPace and stopPace skip a syscall that would change nothing.
182 paceArmed bool
183 paceDeadline time.Time
184 paceSignal atomic.Bool
185 paceSignalFn func()
186 paceDepth int
187 paceDirty bool
188 // sink receives the outputs' reports (presented.go).
189 sink *presentSink
190 // framePeriods is the tick's scratch (display goroutine only).
191 framePeriods map[string]time.Duration
192 // frameFire is dueFrames' result slice, reused across ticks.
193 frameFire []string
194 // held buffers wait until no output reads them (release.go); reports
195 // are the outputs' latest ports.OutputPresented.
196 held []heldBuffer
197 // heldLive is releaseHeld's scratch (display goroutine only).
198 heldLive []ports.OutputPresented
199 reports map[string]ports.OutputPresented
200 frames uint64
201 started time.Time
202 // fifoSurfaces have a fifo barrier or queued commits (fifo.go);
203 // lastFlip is each output's latest page flip.
204 fifoSurfaces map[*surface]struct{}
205 applyingGraph bool
206 graphDrawn bool
207 graphFeedback []graphFeedback
208 readinessGeneration uint64
209 lastFlip map[string]time.Time
210 // tokens are the issued xdg-activation tokens (activation.go).
211 tokens map[string]activationToken
212 // foreignExports are the live xdg-foreign handles (foreign.go).
213 foreignExports map[string]*foreignExport
214 surfaces map[*server.Resource]*surface
215 buffers map[*server.Resource]clientBuffer
216 dmabuf *dmabufGlobal
217 // hdrOutputs records confirmed DRM modesets; absent outputs are SDR.
218 hdrOutputs map[string]ports.OutputHDR
219 colorID uint64
220 colorIdentity map[string]uint64
221 colorDescriptions map[*server.Resource]colorDescription
222 colorOutputs map[*colorOutput]struct{}
223 colorFeedbacks map[*colorFeedback]struct{}
224 serial uint32
225 ctx context.Context
226 windows map[ports.WindowID]*window
227 layers map[ports.WindowID]*layerSurface
228 nextWindow ports.WindowID
229 nextPool uint64
230 nextSurface uint64
231 // seat is the keyboard and pointer state (seat.go).
232 seat seatState
233 // eventMu protects only the notification queue, not display-owned window state.
234 // The event queue is unbounded by design: it grows only if core stops draining
235 // Events, which happens only at shutdown (core owns the receiving end). A
236 // stalled core is a bug surfaced by ctx cancellation, not a reason to block
237 // the display goroutine.
238 eventMu sync.Mutex
239 events []ports.ClientEvent
240 eventReady chan struct{}
241 // contentSeq counts each window's contents and damage is its recent
242 // damage; latest is its current content. All three belong to the display
243 // goroutine, as do the outputs' content links (contentlink.go).
244 contentSeq map[ports.WindowID]uint64
245 damage map[ports.WindowID][]ports.SeqDamage
246 latest map[ports.WindowID]ports.SurfaceContent
247 links []*contentLink
248 // flushPosted: a flushLinks is posted and has not started (Return).
249 flushPosted atomic.Bool
250 flushLinksFn func()
251 // feedbacks wait for the flip that shows their content (presentation.go).
252 feedbacks []feedbackWait
253 // Explicit sync (syncobj.go): the render node, imported timelines by
254 // resource, and the acquire point waiter.
255 syncDev syncobjDevice
256 timelines map[*server.Resource]*timeline
257 syncWait *syncWaiter
258 // flips is each output's latest flip (display goroutine).
259 flips map[string]ports.FlipInfo
260 // inhibitors are the live inhibitor objects (inhibit.go);
261 // shortcutWindows and idleWindows what core was told.
262 inhibitors []*inhibitor
263 shortcutWindows, idleWindows map[ports.WindowID]bool
264 // busInhibited is the last IdleInhibited value (idle.go).
265 busInhibited bool
266 // idleNotes and powers are the idle notifications and output power
267 // objects (idle.go); outputsOff the outputs core turned off.
268 idleNotes []*idleNotification
269 clock ports.Clock
270 powers []*outputPower
271 outputsOff map[string]bool
272 outputs []*output
273 outputManagers []*outputManager
274 workspaceManagers []*workspaceManager
275 toplevelManagers []*toplevelManager
276 extToplevelLists []*extToplevelList
277 workspaceSnapshot ports.Workspaces
278 workspaceFrames []*workspaceFrame
279 workspaceIDs *workspaceid.IDs
280 outputHeads ports.OutputHeads
281 outputPlaces ports.Layout
282 managementSerial uint32
283 nextOutputApply uint64
284 outputReplies map[uint64]*outputConfiguration
285 focusedOutput string
286 fractions map[*surface]*fractionalscale.WpFractionalScaleV1
287 // cursorSurface is the wl_pointer.set_cursor surface in use.
288 cursorSurface *surface
289 // cursorMu guards only the latest cursor change for forwardCursors.
290 cursorMu sync.Mutex
291 cursorLatest ports.CursorChange
292 cursorQueued bool
293 cursorReady chan struct{}
294 // Clipboard and primary selection (clipboard.go).
295 selections [2]*clipSource
296 // Drag and drop (dnd.go): the drag in progress, drops waiting for
297 // their finish, and each client's data devices.
298 dnd *dndState
299 dndDrops []*dndTarget
300 dataDevices map[server.Client][]*wayland.DataDevice
301 clipDevices []*clipDevice
302 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
303 // Pointer constraints (constraints.go).
304 regions map[*server.Resource]*region
305 relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
306 // gestures are each client's zwp_pointer_gestures_v1 objects (gestures.go).
307 gestures map[server.Client][]*gestureObject
308 constraints map[*surface]*constraint
309 constraint *constraint // the active one
310 positioners map[*server.Resource]*positioner
311 // Text input and input method (textinput.go): activeText is the
312 // enabled text input the input method serves.
313 textInputs []*textInput
314 activeText *textInput
315 inputMethod *inputMethod
316}
317
318func removeItem[T comparable](list []T, v T) []T {
319 for i, x := range list {
320 if x == v {
321 return append(list[:i], list[i+1:]...)
322 }
323 }
324 return list
325}
326
327func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
328 if opts.RuntimeDir == "" {
329 opts.RuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
330 }
331 if opts.RuntimeDir == "" {
332 return nil, fmt.Errorf("XDG_RUNTIME_DIR is empty")
333 }
334 d, err := server.NewDisplay()
335 if err != nil {
336 return nil, err
337 }
338 name, fd, cleanup, err := listen(opts.RuntimeDir)
339 if err != nil {
340 d.Close()
341 return nil, err
342 }
343 if err = d.AddSocketFD(fd); err != nil {
344 unix.Close(fd)
345 cleanup()
346 d.Close()
347 return nil, err
348 }
349 s := &Server{
350 securityWake: make(chan struct{}, 1),
351 security: opts.Security,
352 securityController: opts.Security,
353 display: d,
354 awaiting: map[string][]*wayland.Callback{},
355 frameDue: map[string]time.Time{},
356 reports: map[string]ports.OutputPresented{},
357 env: linuxProcEnv{},
358 name: name,
359 cleanup: cleanup,
360 log: log,
361 logDamage: opts.LogDamage,
362 channels: ch,
363 surfaces: make(map[*server.Resource]*surface),
364 buffers: make(map[*server.Resource]clientBuffer),
365 windows: make(map[ports.WindowID]*window),
366 layers: make(map[ports.WindowID]*layerSurface),
367 nextWindow: 1,
368 eventReady: make(chan struct{}, 1),
369 contentSeq: make(map[ports.WindowID]uint64),
370 damage: map[ports.WindowID][]ports.SeqDamage{},
371 latest: make(map[ports.WindowID]ports.SurfaceContent),
372 cursorReady: make(chan struct{}, 1),
373 dataSources: map[*server.Resource]*clipSource{},
374 primarySources: map[*server.Resource]*clipSource{},
375 controlSources: map[*server.Resource]*clipSource{},
376 regions: map[*server.Resource]*region{},
377 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
378 gestures: map[server.Client][]*gestureObject{},
379 constraints: map[*surface]*constraint{},
380 positioners: map[*server.Resource]*positioner{},
381 seat: seatState{keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), keyStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), pointerStamps: make(map[*server.Resource][]*inputtimestamps.ZwpInputTimestampsV1), enters: make(map[*server.Resource]uint32), repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay},
382 }
383 // Bound once, before anything can post them (the sync waiter calls
384 // signalPacer from its own goroutine as soon as it exists).
385 s.paceSignalFn = s.paceSignalled
386 s.flushLinksFn = s.flushLinks
387 s.sink = newPresentSink(s)
388 s.capturedRing = newPostRing(d, capturedRingSize, s.applyCaptured)
389 s.casts = opts.Casts
390 s.castSessions, s.castByCore = map[uint64]*castSession{}, map[uint64]*castSession{}
391 s.castRing = newPostRing(d, castRingSize, s.applyCast)
392 s.castInvFn = s.publishCastInventory
393 s.leaseDevices = map[string]*leaseDevice{}
394 s.pendingLeases = map[uint64]*leaseObject{}
395 s.pendingLeaseCards = map[uint64]string{}
396 s.activeLeases = map[leaseKey]*leaseObject{}
397 s.leaseReady = make(chan struct{}, 1)
398 s.workspaceIDs = opts.WorkspaceIDs
399 if s.workspaceIDs == nil {
400 s.workspaceIDs = workspaceid.New()
401 }
402 s.clock = opts.Clock
403 if s.clock == nil {
404 s.clock = clock.System{}
405 }
406 s.captureAllow, s.peer, s.peers = opts.CaptureAllow, opts.peer, map[server.Client]*peerID{}
407 if s.peer == nil {
408 s.peer = linuxPeerExe{}
409 }
410 s.sandboxed, s.securityStop = map[server.Client]*sandbox{}, -1
411 s.captureReplies = map[uint64]func(ports.CaptureDone){}
412 s.captureInflight = map[uint64]struct{}{}
413 s.outputReplies = map[uint64]*outputConfiguration{}
414 s.managementSerial = 1
415 s.captureSources = map[*server.Resource]*captureSource{}
416 s.sessionsByID = map[uint64]*captureSession{}
417 s.captureSessions = map[*captureSession]struct{}{}
418 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
419 s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
420 s.flips = map[string]ports.FlipInfo{}
421 s.timelines = map[*server.Resource]*timeline{}
422 var node *renderNode
423 if opts.syncDev != nil {
424 s.syncDev = opts.syncDev
425 if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
426 cleanup()
427 d.Close()
428 return nil, err
429 }
430 } else if opts.SyncobjNode != "" {
431 if node, err = openSyncobj(opts.SyncobjNode); err != nil {
432 log.Info().Err(err).Msg("explicit sync off")
433 node = nil
434 } else if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
435 node.close()
436 node = nil
437 } else {
438 s.syncDev = node
439 }
440 }
441 // Implicit dma-buf fences also need this waiter when explicit sync is off.
442 if s.syncWait == nil {
443 if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
444 cleanup()
445 d.Close()
446 return nil, err
447 }
448 }
449 s.colorIdentity = map[string]uint64{}
450 s.colorDescriptions = map[*server.Resource]colorDescription{}
451 s.colorOutputs = map[*colorOutput]struct{}{}
452 s.colorFeedbacks = map[*colorFeedback]struct{}{}
453 s.tokens = map[string]activationToken{}
454 s.foreignExports = map[string]*foreignExport{}
455 s.syncWait.log = log
456 s.cleanup = func() {
457 if s.paceReady {
458 unix.Close(s.paceFD)
459 }
460 if s.securityStop >= 0 {
461 unix.Close(s.securityStop)
462 }
463 if s.seat.keymapFD >= 0 {
464 unix.Close(s.seat.keymapFD)
465 }
466 s.dmabuf.close()
467 if s.syncWait != nil {
468 s.syncWait.close()
469 }
470 if node != nil {
471 node.close()
472 }
473 cleanup()
474 }
475 // Run never starts after a failure here: release what it would close.
476 abort := func(err error) (*Server, error) {
477 s.cleanup()
478 s.syncWait.discard()
479 d.Close()
480 return nil, err
481 }
482 if opts.Keymap != "" {
483 fd, size, e := keymapFile(opts.Keymap)
484 if e != nil {
485 return abort(e)
486 }
487 s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
488 }
489 if s.securityStop, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
490 return abort(err)
491 }
492 if err = s.openPaceTimer(); err != nil {
493 return abort(err)
494 }
495 if err = registerGlobals(d, opts, s); err != nil {
496 return abort(err)
497 }
498 for _, p := range opts.Outputs {
499 s.addOutput(p)
500 }
501 return s, nil
502}
503
504// openPaceTimer creates the pace timerfd and registers it with the display
505// loop (before Run, as AddFD requires).
506func (s *Server) openPaceTimer() error {
507 fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC)
508 if err != nil {
509 return err
510 }
511 // libwayland duplicates fd: ours stays open for TimerfdSettime and is
512 // closed by cleanup; the source goes with the display.
513 if _, err = s.display.AddFD(fd, s.onPaceTimer); err != nil {
514 unix.Close(fd)
515 return err
516 }
517 s.paceFD, s.paceReady = fd, true
518 return nil
519}
520
521func (s *Server) SocketName() string { return s.name }
522func (s *Server) Run(ctx context.Context) error {
523 defer s.cleanup()
524 defer func() {
525 for _, device := range s.leaseDevices {
526 if device.fd != nil {
527 device.fd.Close()
528 }
529 }
530 }()
531 s.log.Info().Str("socket", s.name).Msg("starting wayland")
532 s.started = time.Now()
533 s.ctx = ctx
534 var wg sync.WaitGroup
535 wg.Add(13)
536 go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }()
537 go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }()
538 go func() { defer wg.Done(); s.forwardLeases(ctx) }()
539 go func() { defer wg.Done(); s.forwardLeaseRequests(ctx) }()
540 go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()
541 go func() { defer wg.Done(); s.forward(ctx) }()
542 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
543 go func() {
544 defer wg.Done()
545 for {
546 select {
547 case <-ctx.Done():
548 return
549 case <-s.display.Stopped():
550 return
551 case cmd, ok := <-s.channels.Commands:
552 if !ok {
553 return
554 }
555 if ctx.Err() != nil {
556 return
557 }
558 // One display round trip per batch: Do waits for the event
559 // loop, so a round trip per motion capped pointer events at
560 // ~150/s and let a backlog of stale motions build up.
561 batch, open := drainCommands(cmd, s.channels.Commands)
562 if !s.display.Do(func() {
563 for _, c := range batch {
564 if ctx.Err() != nil {
565 return
566 }
567 s.apply(c)
568 }
569 }) || !open {
570 return
571 }
572 }
573 }
574 }()
575 go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
576 go func() { defer wg.Done(); s.forwardOutputHeads(ctx) }()
577 go func() { defer wg.Done(); s.forwardOutputApplied(ctx) }()
578 go func() { defer wg.Done(); s.forwardBusIdle(ctx) }()
579 go func() { defer wg.Done(); s.forwardCaptured(ctx.Done()) }()
580 if s.syncWait != nil {
581 wg.Add(1)
582 go func() { defer wg.Done(); s.syncWait.run(ctx) }()
583 }
584 if s.casts != nil && s.channels.CastCommands != nil {
585 wg.Add(1)
586 go func() { defer wg.Done(); s.forwardCasts(ctx.Done()) }()
587 }
588 s.castInventoryChanged()
589 err := s.display.Run(ctx)
590 s.stopSecurityContexts()
591 wg.Wait()
592 return err
593}
594
595// forwardOutputHeads delivers backend inventory on the display owner goroutine.
596func (s *Server) forwardOutputHeads(ctx context.Context) {
597 for {
598 select {
599 case <-ctx.Done():
600 return
601 case <-s.display.Stopped():
602 return
603 case heads, ok := <-s.channels.OutputHeads:
604 if !ok {
605 return
606 }
607 if !s.display.Do(func() { s.setOutputHeads(heads) }) {
608 return
609 }
610 }
611 }
612}
613
614func (s *Server) forwardOutputApplied(ctx context.Context) {
615 for {
616 select {
617 case <-ctx.Done():
618 return
619 case <-s.display.Stopped():
620 return
621 case result, ok := <-s.channels.OutputApplied:
622 if !ok {
623 return
624 }
625 if !s.display.Do(func() {
626 c := s.outputReplies[result.ID]
627 delete(s.outputReplies, result.ID)
628 if c == nil || !c.res.Alive() {
629 return
630 }
631 if result.Err != nil {
632 c.res.SendFailed()
633 } else {
634 c.res.SendSucceeded()
635 }
636 }) {
637 return
638 }
639 }
640 }
641}
642
643// damageHistory is how many contents a window's damage history covers:
644// enough for a renderer target a few frames old.
645const damageHistory = 4
646
647// damageHistoryCap is the capacity of a history's backing array: one
648// allocation serves damageHistoryCap-damageHistory+1 publications.
649const damageHistoryCap = 8 * damageHistory
650
651// forwardOutputFormats applies the outputs' scanout formats on the
652// display goroutine.
653func (s *Server) forwardOutputFormats(ctx context.Context) {
654 for {
655 select {
656 case <-ctx.Done():
657 return
658 case <-s.display.Stopped():
659 return
660 case f, ok := <-s.channels.OutputFormats:
661 if !ok {
662 return
663 }
664 if !s.display.Do(func() {
665 s.setOutputHDR(f)
666 if s.dmabuf != nil {
667 s.dmabuf.setOutputFormats(f)
668 }
669 }) {
670 return
671 }
672 }
673 }
674}
675
676func (s *Server) emit(ev ports.ClientEvent) {
677 if s.channels.Events == nil {
678 return
679 }
680 s.eventMu.Lock()
681 s.events = append(s.events, ev)
682 s.eventMu.Unlock()
683 select {
684 case s.eventReady <- struct{}{}:
685 default:
686 }
687}
688
689func (s *Server) forward(ctx context.Context) {
690 for {
691 s.eventMu.Lock()
692 if len(s.events) == 0 {
693 s.eventMu.Unlock()
694 select {
695 case <-ctx.Done():
696 return
697 case <-s.eventReady:
698 continue
699 }
700 }
701 ev := s.events[0]
702 s.events[0] = nil
703 s.events = s.events[1:]
704 s.eventMu.Unlock()
705 select {
706 case <-ctx.Done():
707 return
708 case s.channels.Events <- ev:
709 }
710 }
711}
712
713// emitContent publishes a window's content on the display goroutine: it
714// becomes the window's latest content and joins the batch of every attached
715// output (contentlink.go).
716func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
717 s.contentSeq[c.ID]++
718 c.Seq = s.contentSeq[c.ID]
719 // Published histories are immutable: an output may hold one for as long
720 // as it likes. The history grows inside one backing array, appended past
721 // the end of every published slice (never over an element a reader can
722 // see), and moves to a fresh array only when that one is full. Readers
723 // get a capacity-clipped slice, so they cannot append into the spare.
724 old := s.damage[c.ID]
725 if len(old) >= damageHistory {
726 old = old[len(old)-damageHistory+1:]
727 }
728 if len(old) == cap(old) {
729 grown := make([]ports.SeqDamage, len(old), damageHistoryCap)
730 copy(grown, old)
731 old = grown
732 }
733 h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
734 c.DamageHistory = h[:len(h):len(h)]
735 if c.Empty() {
736 delete(s.damage, c.ID)
737 delete(s.latest, c.ID)
738 } else {
739 s.damage[c.ID] = h
740 s.latest[c.ID] = c
741 }
742 for _, l := range s.links {
743 l.add(c)
744 s.flushLink(l)
745 }
746 s.castCommitted(c.ID)
747}
748
749// maxCommandBatch bounds how many queued commands one display round trip applies.
750const maxCommandBatch = 256
751
752// drainCommands returns first plus the commands already queued, with runs of
753// pointer motion for the same window collapsed into the latest one, their
754// relative deltas summed. open is
755// false once the channel is closed.
756func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (batch []ports.ClientCommand, open bool) {
757 batch = append(batch, first)
758 for len(batch) < maxCommandBatch {
759 select {
760 case c, ok := <-cmds:
761 if !ok {
762 return batch, false
763 }
764 if m, isMotion := c.(ports.PointerMotionTo); isMotion {
765 if last, lastMotion := batch[len(batch)-1].(ports.PointerMotionTo); lastMotion && last.ID == m.ID {
766 // The position is the latest; relative deltas add up so
767 // locked pointers (games) lose no movement.
768 m.DX += last.DX
769 m.DY += last.DY
770 m.UnaccelDX += last.UnaccelDX
771 m.UnaccelDY += last.UnaccelDY
772 batch[len(batch)-1] = m
773 continue
774 }
775 }
776 batch = append(batch, c)
777 default:
778 return batch, true
779 }
780 }
781 return batch, true
782}
783
784func (s *Server) apply(cmd ports.ClientCommand) {
785 if envelope, ok := cmd.(ports.SecurityCommand); ok {
786 if s.security == nil || envelope.State != s.security.Snapshot() {
787 return
788 }
789 cmd = envelope.Command
790 } else if s.security != nil && s.security.Snapshot().Generation != 0 {
791 // Only input/focus commands require an owner envelope. Config relay
792 // also produces SetKeymap and other epoch-independent commands.
793 switch cmd.(type) {
794 case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey,
795 ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
796 return
797 }
798 }
799 switch c := cmd.(type) {
800 case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
801 s.applyInput(c)
802 case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
803 s.applyDrag(c)
804 case ports.ConfigurePopup:
805 if w := s.windows[c.ID]; w != nil && w.popup != nil {
806 w.popup.configure(c)
807 }
808 case ports.ClosePopup:
809 if w := s.windows[c.ID]; w != nil && w.popup != nil {
810 w.popup.dismiss()
811 }
812 case ports.ConfigureWindow:
813 w := s.windows[c.ID]
814 if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
815 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
816 return
817 }
818 s.log.Info().Uint64("id", uint64(c.ID)).Int("w", c.Width).Int("h", c.Height).Bool("fullscreen", c.Fullscreen).Bool("activated", c.Activated).Msg("configure")
819 // Callbacks move when the tree stops or starts being throttled: on
820 // Visible or on Captured.
821 visibilityChanged := w.hasLast && (w.last.Visible != c.Visible || w.last.Captured != c.Captured)
822 scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible
823 w.last, w.hasLast = c, true
824 if visibilityChanged {
825 s.relocateCallbacks(w.xdg.surface)
826 s.dropFeedbacks(time.Now())
827 s.wakePacer() // reconsider FIFO barriers at the new period
828 }
829 w.sendConfigure()
830 s.toplevelChanged(w)
831 if scanoutChanged && w.xdg.surface != nil {
832 s.dmabuf.resendSurface(w.xdg.surface)
833 }
834 if surf := w.xdg.surface; surf != nil && c.Output != "" {
835 surf.sendTreeScale()
836 }
837 if scanoutChanged {
838 s.notifyColorFeedback()
839 }
840 case ports.SetOutputs:
841 s.setOutputs(c)
842 off := map[string]bool{}
843 for _, name := range c.Off {
844 off[name] = true
845 }
846 s.setOutputsOff(off)
847 case ports.CaptureSessionState:
848 s.captureState(c)
849 case ports.UserActivity:
850 s.userActivity()
851 case ports.SlotsPending:
852 s.slotsPending = c.Pending
853 case ports.ShortcutsInhibitState:
854 s.setShortcutsInhibit(c)
855 case ports.CloseWindow:
856 w := s.windows[c.ID]
857 if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() {
858 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window")
859 return
860 }
861 w.toplevel.SendClose()
862 }
863}