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