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