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