feat/data-dnd e05cb15

server.go RawHistoryFolder

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	castInvFn     func()
139	castInvPosted bool
140	castInvSent   bool
141	castInvLast   ports.CastInventory
142	nextSession   uint64
143	excl          *exclusion
144	ownUID        uint32
145	// captureAllow, peer and peers decide mayCapture (captureauth.go):
146	// peers keeps each client's pidfd until it is destroyed.
147	captureAllow *captureallow.Store
148	peer         peerExe
149	peers        map[server.Client]*peerID
150	// noPidfdWarned: the missing SO_PEERPIDFD is logged once.
151	noPidfdWarned bool
152	// sandboxed are the clients that connected through a security context
153	// (securitycontext.go); display goroutine only. securityStop is
154	// written once at shutdown to end the listeners, securityWG waits for
155	// them, securityLive counts them.
156	sandboxed    map[server.Client]*sandbox
157	securityStop int
158	securityWG   sync.WaitGroup
159	securityLive atomic.Int32
160	display      *server.Display
161	env          procEnv
162	slotsPending bool // core waits for a slot window
163	name         string
164	cleanup      func()
165	log          zerowrap.Logger
166	logDamage    bool
167	channels     Channels
168	// awaiting holds frame callbacks by output name, due at its next
169	// frame (frameDue, or its page flip).
170	awaiting    map[string][]*wayland.Callback
171	frameOwners map[*wayland.Callback]*surface
172	frameDue    map[string]time.Time
173	// paceFD is the pace deadline timerfd, armed by ticks on the display
174	// goroutine (paceReady: registered with the display loop);
175	// paceDepth/paceDirty defer wakePacer inside a commit or tick (display
176	// goroutine only). paceSignal coalesces posted ticks (paceSignalFn).
177	paceFD     int
178	paceReady  bool
179	paceWarned bool // a pace timer failure was logged (once)
180	// paceArmed: the timerfd is armed for paceDeadline (display goroutine
181	// only); armPace and stopPace skip a syscall that would change nothing.
182	paceArmed    bool
183	paceDeadline time.Time
184	paceSignal   atomic.Bool
185	paceSignalFn func()
186	paceDepth    int
187	paceDirty    bool
188	// sink receives the outputs' reports (presented.go).
189	sink *presentSink
190	// framePeriods is the tick's scratch (display goroutine only).
191	framePeriods map[string]time.Duration
192	// frameFire is dueFrames' result slice, reused across ticks.
193	frameFire []string
194	// held buffers wait until no output reads them (release.go); reports
195	// are the outputs' latest ports.OutputPresented.
196	held []heldBuffer
197	// heldLive is releaseHeld's scratch (display goroutine only).
198	heldLive []ports.OutputPresented
199	reports  map[string]ports.OutputPresented
200	frames   uint64
201	started  time.Time
202	// fifoSurfaces have a fifo barrier or queued commits (fifo.go);
203	// lastFlip is each output's latest page flip.
204	fifoSurfaces        map[*surface]struct{}
205	applyingGraph       bool
206	graphDrawn          bool
207	graphFeedback       []graphFeedback
208	readinessGeneration uint64
209	lastFlip            map[string]time.Time
210	// tokens are the issued xdg-activation tokens (activation.go).
211	tokens map[string]activationToken
212	// foreignExports are the live xdg-foreign handles (foreign.go).
213	foreignExports map[string]*foreignExport
214	surfaces       map[*server.Resource]*surface
215	buffers        map[*server.Resource]clientBuffer
216	dmabuf         *dmabufGlobal
217	// hdrOutputs records confirmed DRM modesets; absent outputs are SDR.
218	hdrOutputs        map[string]ports.OutputHDR
219	colorID           uint64
220	colorIdentity     map[string]uint64
221	colorDescriptions map[*server.Resource]colorDescription
222	colorOutputs      map[*colorOutput]struct{}
223	colorFeedbacks    map[*colorFeedback]struct{}
224	serial            uint32
225	ctx               context.Context
226	windows           map[ports.WindowID]*window
227	layers            map[ports.WindowID]*layerSurface
228	nextWindow        ports.WindowID
229	nextPool          uint64
230	nextSurface       uint64
231	// seat is the keyboard and pointer state (seat.go).
232	seat seatState
233	// eventMu protects only the notification queue, not display-owned window state.
234	// The event queue is unbounded by design: it grows only if core stops draining
235	// Events, which happens only at shutdown (core owns the receiving end). A
236	// stalled core is a bug surfaced by ctx cancellation, not a reason to block
237	// the display goroutine.
238	eventMu    sync.Mutex
239	events     []ports.ClientEvent
240	eventReady chan struct{}
241	// contentSeq counts each window's contents and damage is its recent
242	// damage; latest is its current content. All three belong to the display
243	// goroutine, as do the outputs' content links (contentlink.go).
244	contentSeq map[ports.WindowID]uint64
245	damage     map[ports.WindowID][]ports.SeqDamage
246	latest     map[ports.WindowID]ports.SurfaceContent
247	links      []*contentLink
248	// flushPosted: a flushLinks is posted and has not started (Return).
249	flushPosted  atomic.Bool
250	flushLinksFn func()
251	// feedbacks wait for the flip that shows their content (presentation.go).
252	feedbacks []feedbackWait
253	// Explicit sync (syncobj.go): the render node, imported timelines by
254	// resource, and the acquire point waiter.
255	syncDev   syncobjDevice
256	timelines map[*server.Resource]*timeline
257	syncWait  *syncWaiter
258	// flips is each output's latest flip (display goroutine).
259	flips map[string]ports.FlipInfo
260	// inhibitors are the live inhibitor objects (inhibit.go);
261	// shortcutWindows and idleWindows what core was told.
262	inhibitors                   []*inhibitor
263	shortcutWindows, idleWindows map[ports.WindowID]bool
264	// busInhibited is the last IdleInhibited value (idle.go).
265	busInhibited bool
266	// idleNotes and powers are the idle notifications and output power
267	// objects (idle.go); outputsOff the outputs core turned off.
268	idleNotes         []*idleNotification
269	clock             ports.Clock
270	powers            []*outputPower
271	outputsOff        map[string]bool
272	outputs           []*output
273	outputManagers    []*outputManager
274	workspaceManagers []*workspaceManager
275	toplevelManagers  []*toplevelManager
276	extToplevelLists  []*extToplevelList
277	workspaceSnapshot ports.Workspaces
278	workspaceFrames   []*workspaceFrame
279	workspaceIDs      *workspaceid.IDs
280	outputHeads       ports.OutputHeads
281	outputPlaces      ports.Layout
282	managementSerial  uint32
283	nextOutputApply   uint64
284	outputReplies     map[uint64]*outputConfiguration
285	focusedOutput     string
286	fractions         map[*surface]*fractionalscale.WpFractionalScaleV1
287	// cursorSurface is the wl_pointer.set_cursor surface in use.
288	cursorSurface *surface
289	// cursorMu guards only the latest cursor change for forwardCursors.
290	cursorMu     sync.Mutex
291	cursorLatest ports.CursorChange
292	cursorQueued bool
293	cursorReady  chan struct{}
294	// Clipboard and primary selection (clipboard.go).
295	selections [2]*clipSource
296	// Drag and drop (dnd.go): the drag in progress, drops waiting for
297	// their finish, and each client's data devices.
298	dnd                                         *dndState
299	dndDrops                                    []*dndTarget
300	dataDevices                                 map[server.Client][]*wayland.DataDevice
301	clipDevices                                 []*clipDevice
302	dataSources, primarySources, controlSources map[*server.Resource]*clipSource
303	// Pointer constraints (constraints.go).
304	regions   map[*server.Resource]*region
305	relatives map[server.Client][]*relativepointer.ZwpRelativePointerV1
306	// gestures are each client's zwp_pointer_gestures_v1 objects (gestures.go).
307	gestures    map[server.Client][]*gestureObject
308	constraints map[*surface]*constraint
309	constraint  *constraint // the active one
310	positioners map[*server.Resource]*positioner
311	// Text input and input method (textinput.go): activeText is the
312	// enabled text input the input method serves.
313	textInputs  []*textInput
314	activeText  *textInput
315	inputMethod *inputMethod
316}
317
318func removeItem[T comparable](list []T, v T) []T {
319	for i, x := range list {
320		if x == v {
321			return append(list[:i], list[i+1:]...)
322		}
323	}
324	return list
325}
326
327func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
328	if opts.RuntimeDir == "" {
329		opts.RuntimeDir = os.Getenv("XDG_RUNTIME_DIR")
330	}
331	if opts.RuntimeDir == "" {
332		return nil, fmt.Errorf("XDG_RUNTIME_DIR is empty")
333	}
334	d, err := server.NewDisplay()
335	if err != nil {
336		return nil, err
337	}
338	name, fd, cleanup, err := listen(opts.RuntimeDir)
339	if err != nil {
340		d.Close()
341		return nil, err
342	}
343	if err = d.AddSocketFD(fd); err != nil {
344		unix.Close(fd)
345		cleanup()
346		d.Close()
347		return nil, err
348	}
349	s := &Server{
350		securityWake:       make(chan struct{}, 1),
351		security:           opts.Security,
352		securityController: opts.Security,
353		display:            d,
354		awaiting:           map[string][]*wayland.Callback{},
355		frameDue:           map[string]time.Time{},
356		reports:            map[string]ports.OutputPresented{},
357		env:                linuxProcEnv{},
358		name:               name,
359		cleanup:            cleanup,
360		log:                log,
361		logDamage:          opts.LogDamage,
362		channels:           ch,
363		surfaces:           make(map[*server.Resource]*surface),
364		buffers:            make(map[*server.Resource]clientBuffer),
365		windows:            make(map[ports.WindowID]*window),
366		layers:             make(map[ports.WindowID]*layerSurface),
367		nextWindow:         1,
368		eventReady:         make(chan struct{}, 1),
369		contentSeq:         make(map[ports.WindowID]uint64),
370		damage:             map[ports.WindowID][]ports.SeqDamage{},
371		latest:             make(map[ports.WindowID]ports.SurfaceContent),
372		cursorReady:        make(chan struct{}, 1),
373		dataSources:        map[*server.Resource]*clipSource{},
374		primarySources:     map[*server.Resource]*clipSource{},
375		controlSources:     map[*server.Resource]*clipSource{},
376		regions:            map[*server.Resource]*region{},
377		relatives:          map[server.Client][]*relativepointer.ZwpRelativePointerV1{},
378		gestures:           map[server.Client][]*gestureObject{},
379		constraints:        map[*surface]*constraint{},
380		positioners:        map[*server.Resource]*positioner{},
381		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},
382	}
383	// Bound once, before anything can post them (the sync waiter calls
384	// signalPacer from its own goroutine as soon as it exists).
385	s.paceSignalFn = s.paceSignalled
386	s.flushLinksFn = s.flushLinks
387	s.sink = newPresentSink(s)
388	s.capturedRing = newPostRing(d, capturedRingSize, s.applyCaptured)
389	s.casts = opts.Casts
390	s.castSessions, s.castByCore = map[uint64]*castSession{}, map[uint64]*castSession{}
391	s.castRing = newPostRing(d, castRingSize, s.applyCast)
392	s.castInvFn = s.publishCastInventory
393	s.leaseDevices = map[string]*leaseDevice{}
394	s.pendingLeases = map[uint64]*leaseObject{}
395	s.pendingLeaseCards = map[uint64]string{}
396	s.activeLeases = map[leaseKey]*leaseObject{}
397	s.leaseReady = make(chan struct{}, 1)
398	s.workspaceIDs = opts.WorkspaceIDs
399	if s.workspaceIDs == nil {
400		s.workspaceIDs = workspaceid.New()
401	}
402	s.clock = opts.Clock
403	if s.clock == nil {
404		s.clock = clock.System{}
405	}
406	s.captureAllow, s.peer, s.peers = opts.CaptureAllow, opts.peer, map[server.Client]*peerID{}
407	if s.peer == nil {
408		s.peer = linuxPeerExe{}
409	}
410	s.sandboxed, s.securityStop = map[server.Client]*sandbox{}, -1
411	s.captureReplies = map[uint64]func(ports.CaptureDone){}
412	s.captureInflight = map[uint64]struct{}{}
413	s.outputReplies = map[uint64]*outputConfiguration{}
414	s.managementSerial = 1
415	s.captureSources = map[*server.Resource]*captureSource{}
416	s.sessionsByID = map[uint64]*captureSession{}
417	s.captureSessions = map[*captureSession]struct{}{}
418	s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
419	s.fifoSurfaces, s.lastFlip = map[*surface]struct{}{}, map[string]time.Time{}
420	s.flips = map[string]ports.FlipInfo{}
421	s.timelines = map[*server.Resource]*timeline{}
422	var node *renderNode
423	if opts.syncDev != nil {
424		s.syncDev = opts.syncDev
425		if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
426			cleanup()
427			d.Close()
428			return nil, err
429		}
430	} else if opts.SyncobjNode != "" {
431		if node, err = openSyncobj(opts.SyncobjNode); err != nil {
432			log.Info().Err(err).Msg("explicit sync off")
433			node = nil
434		} else if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
435			node.close()
436			node = nil
437		} else {
438			s.syncDev = node
439		}
440	}
441	// Implicit dma-buf fences also need this waiter when explicit sync is off.
442	if s.syncWait == nil {
443		if s.syncWait, err = newSyncWaiter(s.signalPacer); err != nil {
444			cleanup()
445			d.Close()
446			return nil, err
447		}
448	}
449	s.colorIdentity = map[string]uint64{}
450	s.colorDescriptions = map[*server.Resource]colorDescription{}
451	s.colorOutputs = map[*colorOutput]struct{}{}
452	s.colorFeedbacks = map[*colorFeedback]struct{}{}
453	s.tokens = map[string]activationToken{}
454	s.foreignExports = map[string]*foreignExport{}
455	s.syncWait.log = log
456	s.cleanup = func() {
457		if s.paceReady {
458			unix.Close(s.paceFD)
459		}
460		if s.securityStop >= 0 {
461			unix.Close(s.securityStop)
462		}
463		if s.seat.keymapFD >= 0 {
464			unix.Close(s.seat.keymapFD)
465		}
466		s.dmabuf.close()
467		if s.syncWait != nil {
468			s.syncWait.close()
469		}
470		if node != nil {
471			node.close()
472		}
473		cleanup()
474	}
475	// Run never starts after a failure here: release what it would close.
476	abort := func(err error) (*Server, error) {
477		s.cleanup()
478		s.syncWait.discard()
479		d.Close()
480		return nil, err
481	}
482	if opts.Keymap != "" {
483		fd, size, e := keymapFile(opts.Keymap)
484		if e != nil {
485			return abort(e)
486		}
487		s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
488	}
489	if s.securityStop, err = unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK); err != nil {
490		return abort(err)
491	}
492	if err = s.openPaceTimer(); err != nil {
493		return abort(err)
494	}
495	if err = registerGlobals(d, opts, s); err != nil {
496		return abort(err)
497	}
498	for _, p := range opts.Outputs {
499		s.addOutput(p)
500	}
501	return s, nil
502}
503
504// openPaceTimer creates the pace timerfd and registers it with the display
505// loop (before Run, as AddFD requires).
506func (s *Server) openPaceTimer() error {
507	fd, err := unix.TimerfdCreate(unix.CLOCK_MONOTONIC, unix.TFD_NONBLOCK|unix.TFD_CLOEXEC)
508	if err != nil {
509		return err
510	}
511	// libwayland duplicates fd: ours stays open for TimerfdSettime and is
512	// closed by cleanup; the source goes with the display.
513	if _, err = s.display.AddFD(fd, s.onPaceTimer); err != nil {
514		unix.Close(fd)
515		return err
516	}
517	s.paceFD, s.paceReady = fd, true
518	return nil
519}
520
521func (s *Server) SocketName() string { return s.name }
522func (s *Server) Run(ctx context.Context) error {
523	defer s.cleanup()
524	defer func() {
525		for _, device := range s.leaseDevices {
526			if device.fd != nil {
527				device.fd.Close()
528			}
529		}
530	}()
531	s.log.Info().Str("socket", s.name).Msg("starting wayland")
532	s.started = time.Now()
533	s.ctx = ctx
534	var wg sync.WaitGroup
535	wg.Add(13)
536	go func() { defer wg.Done(); s.forwardBackendSecurity(ctx) }()
537	go func() { defer wg.Done(); s.forwardSecurityEvents(ctx) }()
538	go func() { defer wg.Done(); s.forwardLeases(ctx) }()
539	go func() { defer wg.Done(); s.forwardLeaseRequests(ctx) }()
540	go func() { defer wg.Done(); s.forwardWorkspaces(ctx) }()
541	go func() { defer wg.Done(); s.forward(ctx) }()
542	go func() { defer wg.Done(); s.forwardCursors(ctx) }()
543	go func() {
544		defer wg.Done()
545		for {
546			select {
547			case <-ctx.Done():
548				return
549			case <-s.display.Stopped():
550				return
551			case cmd, ok := <-s.channels.Commands:
552				if !ok {
553					return
554				}
555				if ctx.Err() != nil {
556					return
557				}
558				// One display round trip per batch: Do waits for the event
559				// loop, so a round trip per motion capped pointer events at
560				// ~150/s and let a backlog of stale motions build up.
561				batch, open := drainCommands(cmd, s.channels.Commands)
562				if !s.display.Do(func() {
563					for _, c := range batch {
564						if ctx.Err() != nil {
565							return
566						}
567						s.apply(c)
568					}
569				}) || !open {
570					return
571				}
572			}
573		}
574	}()
575	go func() { defer wg.Done(); s.forwardOutputFormats(ctx) }()
576	go func() { defer wg.Done(); s.forwardOutputHeads(ctx) }()
577	go func() { defer wg.Done(); s.forwardOutputApplied(ctx) }()
578	go func() { defer wg.Done(); s.forwardBusIdle(ctx) }()
579	go func() { defer wg.Done(); s.forwardCaptured(ctx.Done()) }()
580	if s.syncWait != nil {
581		wg.Add(1)
582		go func() { defer wg.Done(); s.syncWait.run(ctx) }()
583	}
584	if s.casts != nil && s.channels.CastCommands != nil {
585		wg.Add(1)
586		go func() { defer wg.Done(); s.forwardCasts(ctx.Done()) }()
587	}
588	s.castInventoryChanged()
589	err := s.display.Run(ctx)
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
749// maxCommandBatch bounds how many queued commands one display round trip applies.
750const maxCommandBatch = 256
751
752// drainCommands returns first plus the commands already queued, with runs of
753// pointer motion for the same window collapsed into the latest one, their
754// relative deltas summed. open is
755// false once the channel is closed.
756func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (batch []ports.ClientCommand, open bool) {
757	batch = append(batch, first)
758	for len(batch) < maxCommandBatch {
759		select {
760		case c, ok := <-cmds:
761			if !ok {
762				return batch, false
763			}
764			if m, isMotion := c.(ports.PointerMotionTo); isMotion {
765				if last, lastMotion := batch[len(batch)-1].(ports.PointerMotionTo); lastMotion && last.ID == m.ID {
766					// The position is the latest; relative deltas add up so
767					// locked pointers (games) lose no movement.
768					m.DX += last.DX
769					m.DY += last.DY
770					m.UnaccelDX += last.UnaccelDX
771					m.UnaccelDY += last.UnaccelDY
772					batch[len(batch)-1] = m
773					continue
774				}
775			}
776			batch = append(batch, c)
777		default:
778			return batch, true
779		}
780	}
781	return batch, true
782}
783
784func (s *Server) apply(cmd ports.ClientCommand) {
785	if envelope, ok := cmd.(ports.SecurityCommand); ok {
786		if s.security == nil || envelope.State != s.security.Snapshot() {
787			return
788		}
789		cmd = envelope.Command
790	} else if s.security != nil && s.security.Snapshot().Generation != 0 {
791		// Only input/focus commands require an owner envelope. Config relay
792		// also produces SetKeymap and other epoch-independent commands.
793		switch cmd.(type) {
794		case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.FocusWindow, ports.ForwardKey,
795			ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
796			return
797		}
798	}
799	switch c := cmd.(type) {
800	case ports.PointerFocus, ports.PointerMotionTo, ports.PointerButtonTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo, ports.SetKeymap, ports.FocusWindow, ports.ForwardKey:
801		s.applyInput(c)
802	case ports.DataDragFocus, ports.DataDragMotion, ports.DataDragDrop, ports.DataDragCancel:
803		s.applyDrag(c)
804	case ports.ConfigurePopup:
805		if w := s.windows[c.ID]; w != nil && w.popup != nil {
806			w.popup.configure(c)
807		}
808	case ports.ClosePopup:
809		if w := s.windows[c.ID]; w != nil && w.popup != nil {
810			w.popup.dismiss()
811		}
812	case ports.ConfigureWindow:
813		w := s.windows[c.ID]
814		if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() || !w.xdg.resource.Resource.Alive() {
815			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("configure missing window")
816			return
817		}
818		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")
819		// Callbacks move when the tree stops or starts being throttled: on
820		// Visible or on Captured.
821		visibilityChanged := w.hasLast && (w.last.Visible != c.Visible || w.last.Captured != c.Captured)
822		scanoutChanged := !w.hasLast || w.last.Fullscreen != c.Fullscreen || w.last.Output != c.Output || w.last.Visible != c.Visible
823		w.last, w.hasLast = c, true
824		if visibilityChanged {
825			s.relocateCallbacks(w.xdg.surface)
826			s.dropFeedbacks(time.Now())
827			s.wakePacer() // reconsider FIFO barriers at the new period
828		}
829		w.sendConfigure()
830		s.toplevelChanged(w)
831		if scanoutChanged && w.xdg.surface != nil {
832			s.dmabuf.resendSurface(w.xdg.surface)
833		}
834		if surf := w.xdg.surface; surf != nil && c.Output != "" {
835			surf.sendTreeScale()
836		}
837		if scanoutChanged {
838			s.notifyColorFeedback()
839		}
840	case ports.SetOutputs:
841		s.setOutputs(c)
842		off := map[string]bool{}
843		for _, name := range c.Off {
844			off[name] = true
845		}
846		s.setOutputsOff(off)
847	case ports.CaptureSessionState:
848		s.captureState(c)
849	case ports.UserActivity:
850		s.userActivity()
851	case ports.SlotsPending:
852		s.slotsPending = c.Pending
853	case ports.ShortcutsInhibitState:
854		s.setShortcutsInhibit(c)
855	case ports.CloseWindow:
856		w := s.windows[c.ID]
857		if w == nil || w.toplevel == nil || !w.toplevel.Resource.Alive() {
858			s.log.Debug().Uint64("id", uint64(c.ID)).Msg("close missing window")
859			return
860		}
861		w.toplevel.SendClose()
862	}
863}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close