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