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 // cursorClient and cursorOverride belong to the display goroutine.
292 cursorClient ports.CursorChange
293 cursorOverride string
294 // cursorMu guards only the latest cursor change for forwardCursors.
295 cursorMu sync.Mutex
296 cursorLatest ports.CursorChange
297 cursorQueued bool
298 cursorReady chan struct{}
299 // Clipboard and primary selection (clipboard.go).
300 selections [2]*clipSource
301 // Drag and drop (dnd.go): the drag in progress, drops waiting for
302 // their finish, and each client's data devices.
303 dnd *dndState
304 dndDrops []*dndTarget
305 dataDevices map[server.Client][]*wayland.DataDevice
306 clipDevices []*clipDevice
307 dataSources, primarySources, controlSources map[*server.Resource]*clipSource
308 // Pointer constraints (constraints.go).
309 regions map[*server.Resource]*region
310 relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
311 // gestures are each client's zwp_pointer_gestures_v1 objects (gestures.go).
312 gestures map[server.Client][]*gestureObject
313 constraints map[*surface]*constraint
314 constraint *constraint // the active one
315 positioners map[*server.Resource]*positioner
316 // Text input and input method (textinput.go): activeText is the
317 // enabled text input the input method serves.
318 textInputs []*textInput
319 activeText *textInput
320 inputMethod *inputMethod
321}
322
323func removeItem[T comparable](list []T, v T) []T {
324 for i, x := range list {
325 if x == v {
326 return append(list[:i], list[i+1:]...)
327 }
328 }
329 return list
330}
331
332func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
333 if opts.RuntimeDir == "" {
334 opts.RuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
335 }
336 if opts.RuntimeDir == "" {
337 return nil, fmt.Errorf("XDG_RUNTIME_DIR is empty")
338 }
339 d, err := server.NewDisplay()
340 if err != nil {
341 return nil, err
342 }
343 name, fd, cleanup, err := listen(opts.RuntimeDir)
344 if err != nil {
345 d.Close()
346 return nil, err
347 }
348 if err = d.AddSocketFD(fd); err != nil {
349 unix.Close(fd)
350 cleanup()
351 d.Close()
352 return nil, err
353 }
354 s := &Server{
355 securityWake: make(chan struct{}, 1),
356 security: opts.Security,
357 securityController: opts.Security,
358 display: d,
359 awaiting: map[string][]*wayland.Callback{},
360 frameDue: map[string]time.Time{},
361 reports: map[string]ports.OutputPresented{},
362 env: linuxProcEnv{},
363 name: name,
364 cleanup: cleanup,
365 log: log,
366 logDamage: opts.LogDamage,
367 channels: ch,
368 surfaces: make(map[*server.Resource]*surface),
369 buffers: make(map[*server.Resource]clientBuffer),
370 windows: make(map[ports.WindowID]*window),
371 layers: make(map[ports.WindowID]*layerSurface),
372 nextWindow: 1,
373 eventReady: make(chan struct{}, 1),
374 contentSeq: make(map[ports.WindowID]uint64),
375 damage: map[ports.WindowID][]ports.SeqDamage{},
376 latest: make(map[ports.WindowID]ports.SurfaceContent),
377 cursorReady: make(chan struct{}, 1),
378 dataSources: map[*server.Resource]*clipSource{},
379 primarySources: map[*server.Resource]*clipSource{},
380 controlSources: map[*server.Resource]*clipSource{},
381 regions: map[*server.Resource]*region{},
382 relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
383 gestures: map[server.Client][]*gestureObject{},
384 constraints: map[*surface]*constraint{},
385 positioners: map[*server.Resource]*positioner{},
386 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},
387 }
388 // Bound once, before anything can post them (the sync waiter calls
389 // signalPacer from its own goroutine as soon as it exists).
390 s.paceSignalFn = s.paceSignalled
391 s.flushLinksFn = s.flushLinks
392 s.sink = newPresentSink(s)
393 s.capturedRing = newPostRing(d, capturedRingSize, s.applyCaptured)
394 s.casts = opts.Casts
395 s.castSessions, s.castByCore = map[uint64]*castSession{}, map[uint64]*castSession{}
396 s.castRing = newPostRing(d, castRingSize, s.applyCast)
397 s.commands = newCommandRing(d, s.applyQueued)
398 s.castInvFn = s.publishCastInventory
399 s.leaseDevices = map[string]*leaseDevice{}
400 s.pendingLeases = map[uint64]*leaseObject{}
401 s.pendingLeaseCards = map[uint64]string{}
402 s.activeLeases = map[leaseKey]*leaseObject{}
403 s.leaseReady = make(chan struct{}, 1)
404 s.workspaceIDs = opts.WorkspaceIDs
405 if s.workspaceIDs == nil {
406 s.workspaceIDs = workspaceid.New()
407 }
408 s.clock = opts.Clock
409 if s.clock == nil {
410 s.clock = clock.System{}
411 }
412 s.captureAllow, s.peer, s.peers = opts.CaptureAllow, opts.peer, map[server.Client]*peerID{}
413 if s.peer == nil {
414 s.peer = linuxPeerExe{}
415 }
416 s.sandboxed, s.securityStop = map[server.Client]*sandbox{}, -1
417 s.captureReplies = map[uint64]func(ports.CaptureDone){}
418 s.captureInflight = map[uint64]struct{}{}
419 s.outputReplies = map[uint64]*outputConfiguration{}
420 s.managementSerial = 1
421 s.captureSources = map[*server.Resource]*captureSource{}
422 s.sessionsByID = map[uint64]*captureSession{}
423 s.captureSessions = map[*captureSession]struct{}{}
424 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
425 s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
426 s.flips = map[string]ports.FlipInfo{}
427 s.timelines = map[*server.Resource]*timeline{}
428 var node *renderNode
429 if opts.syncDev != nil {
430 s.syncDev = opts.syncDev
431 if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
432 cleanup()
433 d.Close()
434 return nil, err
435 }
436 } else if opts.SyncobjNode != "" {
437 if node, err = openSyncobj(opts.SyncobjNode); err != nil {
438 log.Info().Err(err).Msg("explicit sync off")
439 node = nil
440 } else if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
441 node.close()
442 node = nil
443 } else {
444 s.syncDev = node
445 }
446 }
447 // Implicit dma-buf fences also need this waiter when explicit sync is off.
448 if s.syncWait == nil {
449 if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
450 cleanup()
451 d.Close()
452 return nil, err
453 }
454 }
455 s.colorIdentity = map[string]uint64{}
456 s.colorDescriptions = map[*server.Resource]colorDescription{}
457 s.colorOutputs = map[*colorOutput]struct{}{}
458 s.colorFeedbacks = map[*colorFeedback]struct{}{}
459 s.tokens = map[string]activationToken{}
460 s.foreignExports = map[string]*foreignExport{}
461 s.syncWait.log = log
462 s.syncWait.src, s.syncWait.pace = displayAcquireSource{d}, s.acquireSignalled
463 s.cleanup = func() {
464 if s.paceReady {
465 unix.Close(s.paceFD)
466 }
467 if s.securityStop >= 0 {
468 unix.Close(s.securityStop)
469 }
470 if s.seat.keymapFD >= 0 {
471 unix.Close(s.seat.keymapFD)
472 }
473 s.dmabuf.close()
474 if s.syncWait != nil {
475 s.syncWait.close()
476 }
477 if node != nil {
478 node.close()
479 }
480 cleanup()
481 }
482 // Run never starts after a failure here: release what it would close.
483 abort := func(err error) (*Server, error) {
484 s.cleanup()
485 s.syncWait.discard()
486 d.Close()
487 return nil, err
488 }
489 if opts.Keymap != "" {
490 fd, size, e := keymapFile(opts.Keymap)
491 if e != nil {
492 return abort(e)
493 }
494 s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
495 }
496 if s.securityStop, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
497 return abort(err)
498 }
499 if err = s.openPaceTimer(); err != nil {
500 return abort(err)
501 }
502 if err = registerGlobals(d, opts, s); err != nil {
503 return abort(err)
504 }
505 for _, p := range opts.Outputs {
506 s.addOutput(p)
507 }
508 return s, nil
509}
510
511// openPaceTimer creates the pace timerfd and registers it with the display
512// loop (before Run, as AddFD requires).
513func (s *Server) openPaceTimer() error {
514 fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC)
515 if err != nil {
516 return err
517 }
518 // libwayland duplicates fd: ours stays open for TimerfdSettime and is
519 // closed by cleanup; the source goes with the display.
520 if _, err = s.display.AddFD(fd, s.onPaceTimer); err != nil {
521 unix.Close(fd)
522 return err
523 }
524 s.paceFD, s.paceReady = fd, true
525 return nil
526}
527
528func (s *Server) SocketName() string { return s.name }
529func (s *Server) Run(ctx context.Context) error {
530 defer s.cleanup()
531 defer func() {
532 for _, device := range s.leaseDevices {
533 if device.fd != nil {
534 device.fd.Close()
535 }
536 }
537 }()
538 s.log.Info().Str("socket", s.name).Msg("starting wayland")
539 s.started = time.Now()
540 s.ctx = ctx
541 var wg sync.WaitGroup
542 wg.Add(14)
543 go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }()
544 go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }()
545 go func() { defer wg.Done(); s.forwardLeases(ctx) }()
546 go func() { defer wg.Done(); s.forwardLeaseRequests(ctx) }()
547 go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()
548 go func() { defer wg.Done(); s.forward(ctx) }()
549 go func() { defer wg.Done(); s.forwardCursors(ctx) }()
550 go func() {
551 defer wg.Done()
552 for {
553 select {
554 case <-ctx.Done():
555 return
556 case <-s.display.Stopped():
557 return
558 case cmd, ok := <-s.channels.Commands:
559 if !ok {
560 return
561 }
562 if ctx.Err() != nil {
563 return
564 }
565 // Never wait for the display goroutine: a busy display only
566 // fills the ring, and only then does this goroutine wait.
567 if !s.commands.Push(queue(cmd)) {
568 return
569 }
570 }
571 }
572 }()
573 go func() { defer wg.Done(); s.logCommandStats(ctx) }()
574 go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
575 go func() { defer wg.Done(); s.forwardOutputHeads(ctx) }()
576 go func() { defer wg.Done(); s.forwardOutputApplied(ctx) }()
577 go func() { defer wg.Done(); s.forwardBusIdle(ctx) }()
578 go func() { defer wg.Done(); s.forwardCaptured(ctx.Done()) }()
579 if s.syncWait != nil {
580 wg.Add(1)
581 go func() { defer wg.Done(); s.syncWait.run(ctx) }()
582 }
583 if s.casts != nil && s.channels.CastCommands != nil {
584 wg.Add(1)
585 go func() { defer wg.Done(); s.forwardCasts(ctx.Done()) }()
586 }
587 s.castInventoryChanged()
588 err := s.display.Run(ctx)
589 s.syncWait.releasePending()
590 s.stopSecurityContexts()
591 wg.Wait()
592 return err
593}
594
595// forwardOutputHeads delivers backend inventory on the display owner goroutine.
596func (s *Server) forwardOutputHeads(ctx context.Context) {
597 for {
598 select {
599 case <-ctx.Done():
600 return
601 case <-s.display.Stopped():
602 return
603 case heads, ok := <-s.channels.OutputHeads:
604 if !ok {
605 return
606 }
607 if !s.display.Do(func() { s.setOutputHeads(heads) }) {
608 return
609 }
610 }
611 }
612}
613
614func (s *Server) forwardOutputApplied(ctx context.Context) {
615 for {
616 select {
617 case <-ctx.Done():
618 return
619 case <-s.display.Stopped():
620 return
621 case result, ok := <-s.channels.OutputApplied:
622 if !ok {
623 return
624 }
625 if !s.display.Do(func() {
626 c := s.outputReplies[result.ID]
627 delete(s.outputReplies, result.ID)
628 if c == nil || !c.res.Alive() {
629 return
630 }
631 if result.Err != nil {
632 c.res.SendFailed()
633 } else {
634 c.res.SendSucceeded()
635 }
636 }) {
637 return
638 }
639 }
640 }
641}
642
643// damageHistory is how many contents a window's damage history covers:
644// enough for a renderer target a few frames old.
645const damageHistory = 4
646
647// damageHistoryCap is the capacity of a history's backing array: one
648// allocation serves damageHistoryCap-damageHistory+1 publications.
649const damageHistoryCap = 8 * damageHistory
650
651// forwardOutputFormats applies the outputs' scanout formats on the
652// display goroutine.
653func (s *Server) forwardOutputFormats(ctx context.Context) {
654 for {
655 select {
656 case <-ctx.Done():
657 return
658 case <-s.display.Stopped():
659 return
660 case f, ok := <-s.channels.OutputFormats:
661 if !ok {
662 return
663 }
664 if !s.display.Do(func() {
665 s.setOutputHDR(f)
666 if s.dmabuf != nil {
667 s.dmabuf.setOutputFormats(f)
668 }
669 }) {
670 return
671 }
672 }
673 }
674}
675
676func (s *Server) emit(ev ports.ClientEvent) {
677 if s.channels.Events == nil {
678 return
679 }
680 s.eventMu.Lock()
681 s.events = append(s.events, ev)
682 s.eventMu.Unlock()
683 select {
684 case s.eventReady <- struct{}{}:
685 default:
686 }
687}
688
689func (s *Server) forward(ctx context.Context) {
690 for {
691 s.eventMu.Lock()
692 if len(s.events) == 0 {
693 s.eventMu.Unlock()
694 select {
695 case <-ctx.Done():
696 return
697 case <-s.eventReady:
698 continue
699 }
700 }
701 ev := s.events[0]
702 s.events[0] = nil
703 s.events = s.events[1:]
704 s.eventMu.Unlock()
705 select {
706 case <-ctx.Done():
707 return
708 case s.channels.Events <- ev:
709 }
710 }
711}
712
713// emitContent publishes a window's content on the display goroutine: it
714// becomes the window's latest content and joins the batch of every attached
715// output (contentlink.go).
716func (s *Server) emitContent(c ports.SurfaceContent, d damage) {
717 s.contentSeq[c.ID]++
718 c.Seq = s.contentSeq[c.ID]
719 // Published histories are immutable: an output may hold one for as long
720 // as it likes. The history grows inside one backing array, appended past
721 // the end of every published slice (never over an element a reader can
722 // see), and moves to a fresh array only when that one is full. Readers
723 // get a capacity-clipped slice, so they cannot append into the spare.
724 old := s.damage[c.ID]
725 if len(old) >= damageHistory {
726 old = old[len(old)-damageHistory+1:]
727 }
728 if len(old) == cap(old) {
729 grown := make([]ports.SeqDamage, len(old), damageHistoryCap)
730 copy(grown, old)
731 old = grown
732 }
733 h := append(old, ports.SeqDamage{Seq: c.Seq, Full: d.full, Rects: d.rects})
734 c.DamageHistory = h[:len(h):len(h)]
735 if c.Empty() {
736 delete(s.damage, c.ID)
737 delete(s.latest, c.ID)
738 } else {
739 s.damage[c.ID] = h
740 s.latest[c.ID] = c
741 }
742 for _, l := range s.links {
743 l.add(c)
744 s.flushLink(l)
745 }
746 s.castCommitted(c.ID)
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, ports.SetModifiers, ports.CursorOverride,
760 ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
761 return
762 }
763 }
764 switch c := cmd.(type) {
765 case ports.CursorOverride:
766 s.setCursorOverride(c.Shape)
767 case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey, ports.SetModifiers:
768 s.applyInput(c)
769 case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
770 s.applyDrag(c)
771 case ports.ConfigurePopup:
772 if w := s.windows[c.ID]; w != nil && w.popup != nil {
773 w.popup.configure(c)
774 }
775 case ports.ClosePopup:
776 if w := s.windows[c.ID]; w != nil && w.popup != nil {
777 w.popup.dismiss()
778 }
779 case ports.ConfigureWindow:
780 w := s.windows[c.ID]
781 if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
782 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
783 return
784 }
785 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")
786 // Callbacks move when the tree stops or starts being throttled: on
787 // Visible or on Captured.
788 visibilityChanged := w.hasLast && (w.last.Visible != c.Visible || w.last.Captured != c.Captured)
789 scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible
790 w.last, w.hasLast = c, true
791 if visibilityChanged {
792 s.relocateCallbacks(w.xdg.surface)
793 s.dropFeedbacks(time.Now())
794 s.wakePacer() // reconsider FIFO barriers at the new period
795 }
796 w.sendConfigure()
797 s.toplevelChanged(w)
798 if scanoutChanged && w.xdg.surface != nil {
799 s.dmabuf.resendSurface(w.xdg.surface)
800 }
801 if surf := w.xdg.surface; surf != nil && c.Output != "" {
802 surf.sendTreeScale()
803 }
804 if scanoutChanged {
805 s.notifyColorFeedback()
806 }
807 case ports.SetOutputs:
808 s.setOutputs(c)
809 off := map[string]bool{}
810 for _, name := range c.Off {
811 off[name] = true
812 }
813 s.setOutputsOff(off)
814 case ports.CaptureSessionState:
815 s.captureState(c)
816 case ports.UserActivity:
817 s.userActivity()
818 case ports.ShortcutsInhibitState:
819 s.setShortcutsInhibit(c)
820 case ports.CloseWindow:
821 w := s.windows[c.ID]
822 if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() {
823 s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window")
824 return
825 }
826 w.toplevel.SendClose()
827 }
828}