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