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