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