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