fifo.go RawHistoryFolder

1package wayland
2
3import (
4	"sync"
5	"time"
6
7	"github.com/bnema/go-wayland-bindings/server/committiming"
8	"github.com/bnema/go-wayland-bindings/server/contenttype"
9	"github.com/bnema/go-wayland-bindings/server/fifo"
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/bnema/purego-libwayland/server"
13	"golang.org/x/sys/unix"
14)
15
16// Presentation constraints on content updates (games, Vulkan FIFO):
17//
18//   - wp_fifo_v1: a commit with wait_barrier waits while the barrier set
19//     by an earlier commit stands; the barrier clears at the output's
20//     next refresh (its flip, or a deadline when it does not flip).
21//   - wp_commit_timing_v1: a commit waits until the refresh that shows it
22//     is not before its timestamp (CLOCK_MONOTONIC).
23//
24// Every commit captures its pending state into an update; the pacer (frames.go)
25// applies queued updates in order when they are ready. Requests arriving after
26// capture stay in live pending state. wp_content_type_v1 is logged only.
27
28func registerPresentationConstraints(d *server.Display, s *Server) error {
29	if err := fifo.NewWpFifoManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
30		_, _ = fifo.NewWpFifoManagerV1(c, int32(v), id, fifoManager{s})
31	}); err != nil {
32		return err
33	}
34	if err := committiming.NewWpCommitTimingManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
35		_, _ = committiming.NewWpCommitTimingManagerV1(c, int32(v), id, timingManager{s})
36	}); err != nil {
37		return err
38	}
39	return contenttype.NewWpContentTypeManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
40		_, _ = contenttype.NewWpContentTypeManagerV1(c, int32(v), id, contentTypeManager{s})
41	})
42}
43
44// fifo_v1
45
46type fifoManager struct{ server *Server }
47
48func (fifoManager) Destroy(*fifo.WpFifoManagerV1) {}
49
50func (m fifoManager) GetFifo(r *fifo.WpFifoManagerV1, id uint32, surf *wayland.Surface) {
51	state := m.server.surfaceOf(surf)
52	if state != nil && state.fifo != nil {
53		r.PostError(uint32(fifo.WpFifoManagerV1ErrorAlreadyExists), "surface already has a fifo")
54		return
55	}
56	h := &fifoHandler{surface: state}
57	res, err := fifo.NewWpFifoV1(r.Client(), r.Version(), id, h)
58	if err != nil || state == nil {
59		return
60	}
61	state.fifo = h
62	res.OnDestroy = func() {
63		if state.fifo == h {
64			state.fifo = nil
65		}
66	}
67}
68
69type fifoHandler struct{ surface *surface }
70
71func (h *fifoHandler) live(r *fifo.WpFifoV1) bool {
72	if h.surface == nil || h.surface.destroyed {
73		r.PostError(uint32(fifo.WpFifoV1ErrorSurfaceDestroyed), "surface destroyed")
74		return false
75	}
76	return true
77}
78func (h *fifoHandler) SetBarrier(r *fifo.WpFifoV1) {
79	if h.live(r) {
80		h.surface.next.barrier = true
81	}
82}
83func (h *fifoHandler) WaitBarrier(r *fifo.WpFifoV1) {
84	if h.live(r) {
85		h.surface.next.wait = true
86	}
87}
88func (*fifoHandler) Destroy(*fifo.WpFifoV1) {}
89
90// commit_timing_v1
91
92type timingManager struct{ server *Server }
93
94func (timingManager) Destroy(*committiming.WpCommitTimingManagerV1) {}
95
96func (m timingManager) GetTimer(r *committiming.WpCommitTimingManagerV1, id uint32, surf *wayland.Surface) {
97	state := m.server.surfaceOf(surf)
98	if state != nil && state.timer != nil {
99		r.PostError(uint32(committiming.WpCommitTimingManagerV1ErrorCommitTimerExists), "surface already has a commit timer")
100		return
101	}
102	h := &timerHandler{surface: state}
103	res, err := committiming.NewWpCommitTimerV1(r.Client(), r.Version(), id, h)
104	if err != nil || state == nil {
105		return
106	}
107	state.timer = h
108	res.OnDestroy = func() {
109		if state.timer == h {
110			state.timer = nil
111		}
112	}
113}
114
115type timerHandler struct{ surface *surface }
116
117func (h *timerHandler) SetTimestamp(r *committiming.WpCommitTimerV1, secHi, secLo, nsec uint32) {
118	switch {
119	case h.surface == nil || h.surface.destroyed:
120		r.PostError(uint32(committiming.WpCommitTimerV1ErrorSurfaceDestroyed), "surface destroyed")
121	case nsec >= 1e9:
122		r.PostError(uint32(committiming.WpCommitTimerV1ErrorInvalidTimestamp), "tv_nsec out of range")
123	case !h.surface.next.at.IsZero():
124		r.PostError(uint32(committiming.WpCommitTimerV1ErrorTimestampExists), "timestamp already set")
125	default:
126		h.surface.next.at = monotonicTime(uint64(secHi)<<32|uint64(secLo), int64(nsec))
127	}
128}
129func (*timerHandler) Destroy(*committiming.WpCommitTimerV1) {}
130
131// maxTimestampAhead bounds how far a commit timestamp can hold a commit;
132// a later one applies early (a deviation from "not before").
133const maxTimestampAhead = time.Second
134
135// monotonicTime converts a CLOCK_MONOTONIC time to wall time, at most
136// maxTimestampAhead from now: a far timestamp must not stall the surface.
137func monotonicTime(sec uint64, nsec int64) time.Time {
138	var now unix.Timespec
139	if unix.ClockGettime(unix.CLOCK_MONOTONIC, &now) != nil {
140		return time.Now()
141	}
142	ahead := time.Duration(nsec - now.Nsec)
143	switch n := uint64(now.Sec); {
144	case sec >= n+2:
145		ahead = maxTimestampAhead
146	case sec+2 <= n:
147		ahead = 0 // in the past: apply now
148	default:
149		ahead += time.Duration(int64(sec)-now.Sec) * time.Second
150	}
151	return time.Now().Add(min(max(ahead, 0), maxTimestampAhead))
152}
153
154// content_type_v1
155
156type contentTypeManager struct{ server *Server }
157
158func (contentTypeManager) Destroy(*contenttype.WpContentTypeManagerV1) {}
159
160func (m contentTypeManager) GetSurfaceContentType(r *contenttype.WpContentTypeManagerV1, id uint32, surf *wayland.Surface) {
161	state := m.server.surfaceOf(surf)
162	if state != nil && state.contentType != nil {
163		r.PostError(uint32(contenttype.WpContentTypeManagerV1ErrorAlreadyConstructed), "surface already has a content type")
164		return
165	}
166	h := &contentTypeHandler{surface: state}
167	res, err := contenttype.NewWpContentTypeV1(r.Client(), r.Version(), id, h)
168	if err != nil || state == nil {
169		return
170	}
171	state.contentType = h
172	res.OnDestroy = func() {
173		// Destroying it sets the type back to none, double-buffered.
174		if state.contentType == h {
175			state.contentType = nil
176			state.next.kind = 0
177		}
178	}
179}
180
181type contentTypeHandler struct{ surface *surface }
182
183func (h *contentTypeHandler) SetContentType(_ *contenttype.WpContentTypeV1, kind uint32) {
184	if h.surface != nil && !h.surface.destroyed && h.surface.contentType == h {
185		h.surface.next.kind = kind
186	}
187}
188func (*contentTypeHandler) Destroy(*contenttype.WpContentTypeV1) {}
189
190func (s *Server) surfaceOf(w *wayland.Surface) *surface {
191	if w == nil {
192		return nil
193	}
194	return s.surfaces[w.Resource]
195}
196
197// Content update queue
198
199// update owns a captured commit until it applies or is discarded.
200type update struct {
201	// pendingCommit is the wl_surface state captured by the commit.
202	pendingCommit
203	vp              *viewport
204	vpW, vpH        int32
205	vpSet           bool
206	vpSrc           [4]server.Fixed
207	vpCrop          bool
208	layout          []childLayout
209	deps            []*update
210	owner           *surface
211	implicit        [4]*syncWait
212	prev            *update
213	synced, bound   bool
214	xdg             *xdgSurface
215	geometry        ports.Rect
216	cons            *constraint
217	region          *ports.Rect
218	layer           *layerSurface
219	layerNext       layerState
220	lockAck         lockConfigure
221	readyGeneration uint64
222	readyResult     bool
223	// refs counts incoming deps and prev links. Retired updates remain readable
224	// until their last incoming link goes away, even after dropQueue.
225	refs    int
226	retired bool
227	failed  bool // acquire observation failed: discard, never publish
228}
229
230// updatePool contains only retired updates with no incoming graph links. A
231// popped update is NOT reusable just because it left its owner's queue: a
232// parent's queued deps can still point to it (including after role teardown).
233// The display owner goroutine changes graph links; sync.Pool only shares dead,
234// zeroed storage between independent displays.
235var updatePool sync.Pool
236
237func (u *update) releaseRef() {
238	u.refs--
239	if u.refs == 0 && u.retired {
240		deps := u.deps[:0]
241		*u = update{deps: deps}
242		updatePool.Put(u)
243	}
244}
245
246// retireUpdate severs outgoing links only after the update is no longer used
247// by applyUpdate/dropQueue. Incoming links keep its identity until consumed.
248func (u *update) retireUpdate() {
249	u.retired = true
250	prev, deps := u.prev, u.deps
251	u.prev, u.deps = nil, deps[:0]
252	if prev != nil {
253		prev.releaseRef()
254	}
255	for i, dep := range deps {
256		deps[i] = nil // the pooled backing array must not retain old dependencies
257		dep.releaseRef()
258	}
259	if u.refs == 0 {
260		*u = update{deps: u.deps}
261		updatePool.Put(u)
262	}
263}
264
265type childLayout struct {
266	child *surface
267	x, y  int
268	below bool
269}
270
271func removeLayout(layout []childLayout, child *surface) []childLayout {
272	layout = append([]childLayout(nil), layout...)
273	for i := 0; i < len(layout); i++ {
274		if layout[i].child == child {
275			layout = append(layout[:i:i], layout[i+1:]...)
276			i--
277		}
278	}
279	return layout
280}
281
282func sameLayout(a, b []childLayout) bool {
283	if len(a) != len(b) {
284		return false
285	}
286	for i := range a {
287		if a[i] != b[i] {
288			return false
289		}
290	}
291	return true
292}
293
294// takePending moves the pending state into an update: one-shot state is
295// cleared, sticky state (scale, hints, viewport, geometry, layer) kept.
296func (s *surface) takePending() update {
297	u := update{pendingCommit: s.next}
298	// One-shot requests apply once; sticky ones carry over.
299	s.next = pendingCommit{scale: s.next.scale, transform: s.next.transform, async: s.next.async, color: s.next.color, representation: s.next.representation, kind: s.next.kind, fade: s.next.fade}
300	if v := s.viewport; v != nil {
301		u.vp, u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v, v.pendingW, v.pendingH, v.pendingSet, v.pendingSrc, v.pendingCrop
302	}
303	u.layout = s.sub.pendingLayout
304	if s.xdg != nil {
305		u.xdg, u.geometry = s.xdg, s.xdg.pendingGeometry
306	}
307	if c := s.server.constraints[s]; c != nil && c.pending != nil {
308		u.cons, u.region, c.pending = c, c.pending, nil
309	}
310	if s.layer != nil {
311		u.layer, u.layerNext = s.layer, s.layer.pending
312	}
313	if s.lock != nil {
314		u.lockAck = s.lock.ack
315	}
316	return u
317}
318
319// tooEarly reports whether content applied now would show before at:
320// it reaches the screen one refresh later.
321func (s *surface) tooEarly(at, now time.Time) bool {
322	return !at.IsZero() && at.After(s.server.nextRefresh(s.server.fifoOutput(s), now))
323}
324
325// nextRefresh is when the output's next frame reaches the screen: the
326// vblank after now counted from its last flip (kernel time, refresh
327// period), or one period from now without flips or while VRR is on.
328func (s *Server) nextRefresh(name string, now time.Time) time.Time {
329	p := s.outputPeriod(name)
330	f, ok := s.flips[name]
331	if !ok || f.When <= 0 || f.Refresh <= 0 {
332		return now.Add(p)
333	}
334	mono := monotonic(now)
335	since := mono - f.When
336	if since < 0 || since > time.Second {
337		return now.Add(p)
338	}
339	n := since/f.Refresh + 1
340	return now.Add(f.When + n*f.Refresh - mono)
341}
342
343// queueUpdate queues the pending commit until it can apply.
344func (s *surface) queueUpdate() {
345	u, _ := updatePool.Get().(*update)
346	if u == nil {
347		u = new(update)
348	}
349	deps := u.deps[:0]
350	*u = s.takePending()
351	u.deps = deps
352	u.owner, u.synced = s, s.effectivelySynced()
353	if s.server.syncWait != nil && u.attached && u.buffer != nil && u.sync == nil {
354		if b, ok := s.server.buffers[u.buffer.Resource].(*dmabufBuffer); ok {
355			err := s.server.syncWait.watchImplicit(b.buf, &u.implicit)
356			if err != nil {
357				// Partial registrations were cancelled; discard this update.
358				u.failed = true
359				s.server.log.Warn().Str("component", "wayland").Err(err).Msg("implicit buffer wait")
360			}
361		}
362	}
363	for _, ch := range s.sub.children {
364		for i := len(ch.queue) - 1; i >= 0; i-- {
365			candidate := ch.queue[i]
366			if candidate.synced && !candidate.bound {
367				candidate.bound = true
368				u.deps = append(u.deps, candidate)
369				candidate.refs++
370				break
371			}
372		}
373	}
374	if len(s.queue) > 0 {
375		u.prev = s.queue[len(s.queue)-1]
376		u.prev.refs++
377	}
378	s.queue = append(s.queue, u)
379	s.server.fifoSurfaces[s] = struct{}{}
380	s.server.wakePacer()
381}
382
383// applyUpdate applies only the captured commit; later requests remain pending.
384func (s *surface) applyUpdate(u *update) {
385	// A buffer destroyed while queued (a swapchain resize) is skipped: the
386	// surface keeps its content, and is not unmapped behind the client.
387	s.commitSkipped = false
388	s.queuedScale = u.scale
389	s.queuedBuffer = 0
390	if u.buffer != nil {
391		s.queuedBuffer = u.buffer.ID()
392	}
393	if u.buffer != nil && !u.buffer.Resource.Alive() {
394		s.commitSkipped = true
395		// No reader takes this buffer. A release point is safe to signal
396		// only if the acquire has already signalled; dropSync owns that rule.
397		s.dropSync(u.sync)
398		u.sync = nil
399		u.buffer, u.attached = nil, false
400		// Scale, transform and crop describe the same retained buffer.
401		u.scale = s.bufferScale
402		u.transform = s.transform
403		// The viewport change was made for that buffer: keep the crop that
404		// matches the retained content instead of validating a mismatch.
405		if u.vp != nil && (s.lock != nil || u.vp.resource != nil && u.vp.resource.Alive()) {
406			v := &s.committedViewport
407			u.vpW, u.vpH, u.vpSet, u.vpSrc, u.vpCrop = v.destW, v.destH, v.dest, v.src, v.crop
408		}
409	}
410	s.applyCommit(u)
411	s.commitSkipped = false
412}
413
414// dropQueue discards the queued updates of a destroyed surface or role.
415// A live surface still gets its queued frame callbacks.
416func (s *surface) dropQueue() {
417	if s.kind == roleSessionLock {
418		s.dropLockDeps()
419	}
420	s.dropQueuePrefix(len(s.queue))
421}
422
423// dropQueuePrefix retires only the first n updates. Leave the preserved suffix
424// visible to release checks: a buffer shared with it is still client-owned work.
425func (s *surface) dropQueuePrefix(n int) {
426	queue := s.queue[:n]
427	s.queue = s.queue[n:]
428	released := map[*server.Resource]bool{}
429	for _, u := range queue {
430		if s.destroyed {
431			for _, cb := range u.callbacks {
432				cb.Destroy()
433			}
434		} else if len(u.callbacks) > 0 {
435			s.server.queueFrames(s, u.callbacks)
436		}
437		for _, fb := range u.feedback {
438			discard(fb)
439		}
440		s.dropSync(u.sync)
441		for _, w := range u.implicit {
442			s.server.syncWait.cancel(w)
443		}
444		if u.sync != nil {
445			continue // explicit sync: the release point replaced release
446		}
447		if u.buffer != nil && !sameBuffer(u.buffer, s.current) && !released[u.buffer.Resource] && u.buffer.Resource.Alive() && !s.bufferQueuedElsewhere(u.buffer) {
448			released[u.buffer.Resource] = true
449			u.buffer.SendRelease()
450		}
451	}
452	for i, u := range queue {
453		queue[i] = nil // do not retain retired objects in the queue backing array
454		u.retireUpdate()
455	}
456	if len(s.queue) == 0 {
457		s.queue = queue[:0] // preserve steady-state capacity when all were retired
458		delete(s.server.fifoSurfaces, s)
459	}
460}
461
462func (s *surface) bufferQueuedElsewhere(b *wayland.Buffer) bool {
463	for _, other := range s.server.surfaces {
464		if sameBuffer(other.next.buffer, b) {
465			return true
466		}
467		if other != s && sameBuffer(other.current, b) {
468			return true
469		}
470		for _, u := range other.queue {
471			if sameBuffer(u.buffer, b) {
472				return true
473			}
474		}
475	}
476	return false
477}
478
479// setBarrier applies a committed set_barrier.
480func (s *surface) setBarrier(now time.Time) {
481	s.barrier, s.barrierAt = true, now
482	s.server.fifoSurfaces[s] = struct{}{}
483	s.server.wakePacer()
484}
485
486// fifoOutput is the output pacing a surface, "" when none does.
487func (s *Server) fifoOutput(surf *surface) string {
488	name := s.frameOutput(surf)
489	if name != "" && name != suspendedFrameQueue && s.outputByNameExact(name) == nil {
490		return ""
491	}
492	return name
493}
494
495// outputPeriod is one refresh of an output ("": 60 Hz).
496func (s *Server) outputPeriod(name string) time.Duration {
497	if name == suspendedFrameQueue {
498		return suspendedFramePeriod
499	}
500	if o := s.outputByNameExact(name); o != nil && name != "" {
501		return framePeriod(o.place.Info.RefreshMilli)
502	}
503	return defaultFramePeriod
504}
505
506// tickFifo clears the barriers whose latching deadline passed and applies
507// the queued updates now ready. A flip of the surface's output is its
508// deadline; outputs that do not flip (headless, switched away, no output)
509// clear it after one refresh, or 1.5 when they usually flip. It returns
510// the time to the next deadline and whether any surface still waits.
511func (s *Server) tickFifo(now time.Time, flipped map[string]bool) (time.Duration, bool) {
512	for name, f := range flipped {
513		if f {
514			s.lastFlip[name] = now
515		}
516	}
517	for name := range s.lastFlip {
518		if s.outputByNameExact(name) == nil {
519			delete(s.lastFlip, name)
520		}
521	}
522	wait := suspendedFramePeriod
523	s.readinessGeneration++
524	for surf := range s.fifoSurfaces {
525		name := s.fifoOutput(surf)
526		p := s.outputPeriod(name)
527		// The deadline of outputs that usually flip only catches a missed flip.
528		deadline := surf.barrierAt.Add(p)
529		if name != "" && now.Sub(s.lastFlip[name]) < time.Second {
530			deadline = deadline.Add(p / 2)
531		}
532		if surf.barrier && ((flipped[name] && name != "" && s.lastFlip[name].After(surf.barrierAt)) || !now.Before(deadline)) {
533			surf.barrier = false
534		}
535		for len(surf.queue) > 0 && !surf.destroyed {
536			u := surf.queue[0]
537			if u.synced || !u.graphReady(now) {
538				break
539			}
540			s.applyingGraph = true
541			s.graphDrawn = false
542			s.graphFeedback = s.graphFeedback[:0]
543			u.applyGraph()
544			s.applyingGraph = false
545			// Applying a graph changes queues and readiness within this tick.
546			s.readinessGeneration++
547			if s.graphDrawn {
548				surf.redraw()
549			}
550			// Only the last commit per surface can be sampled from this
551			// publication. Earlier callbacks were already queued on apply.
552			for i, fb := range s.graphFeedback {
553				superseded := false
554				for j := i + 1; j < len(s.graphFeedback); j++ {
555					if s.graphFeedback[j].surf == fb.surf && s.graphFeedback[j].fresh {
556						superseded = true
557						break
558					}
559				}
560				if superseded {
561					for _, pending := range fb.pending {
562						discard(pending)
563					}
564					continue
565				}
566				fb.surf.commitFeedback(fb.pending, fb.fresh)
567			}
568			s.graphFeedback = s.graphFeedback[:0]
569		}
570		if surf.barrier {
571			wait = min(wait, deadline.Sub(now))
572		}
573		if len(surf.queue) > 0 {
574			wait = min(wait, p)
575		}
576		if !surf.barrier && len(surf.queue) == 0 {
577			delete(s.fifoSurfaces, surf)
578		}
579	}
580	return max(wait, time.Millisecond), len(s.fifoSurfaces) > 0
581}
582
583func (s *surface) effectivelySynced() bool {
584	for p := s; p != nil; p = p.sub.parent {
585		if p.sub.parent != nil && p.sub.synced {
586			return true
587		}
588	}
589	return false
590}
591
592func (s *surface) flushDesync() {
593	if !s.effectivelySynced() {
594		for _, u := range s.queue {
595			if u.synced && !u.bound {
596				u.synced = false
597			}
598		}
599	}
600	for _, ch := range s.sub.children {
601		ch.flushDesync()
602	}
603	s.server.wakePacer()
604}
605
606func (u *update) graphReady(now time.Time) bool {
607	if u.retired {
608		return true // applied or dropped; a queued parent cannot replay it
609	}
610	s := u.owner
611	gen := s.server.readinessGeneration
612	if gen != 0 && u.readyGeneration == gen {
613		return u.readyResult
614	}
615	if gen != 0 {
616		u.readyGeneration, u.readyResult = gen, false
617	}
618	if u.prev != nil && !u.prev.graphReady(now) {
619		return false
620	}
621	if u.wait && s.barrier || s.tooEarly(u.at, now) {
622		return false
623	}
624	// A dead attachment is skipped by applyUpdate; waiting for its fence
625	// would leave this update and every later one stuck behind it.
626	if u.buffer != nil && !u.buffer.Resource.Alive() {
627		for i, w := range u.implicit {
628			if w != nil {
629				s.server.syncWait.cancel(w)
630				u.implicit[i] = nil
631			}
632		}
633	} else if !u.failed {
634		if !s.syncReady(u.sync) {
635			return false
636		}
637		if u.sync != nil && u.sync.wait != nil && u.sync.wait.failed {
638			u.failed = true
639		}
640		waiting := false
641		for _, w := range u.implicit {
642			if w == nil {
643				continue
644			}
645			if !s.server.syncWait.fired(w) {
646				waiting = true
647			} else if w.failed {
648				u.failed = true
649			}
650		}
651		if waiting && !u.failed {
652			return false
653		}
654	}
655	if u.failed {
656		u.readyResult = true // ready only to retire, never to apply
657		return true
658	}
659	for _, dep := range u.deps {
660		if !dep.owner.destroyed && !dep.graphReady(now) {
661			return false
662		}
663	}
664	u.readyResult = true
665	return true
666}
667
668func (u *update) applyGraph() {
669	if u.retired {
670		return
671	}
672	s := u.owner
673	if !u.failed && u.sync != nil && (u.buffer == nil || u.buffer.Alive()) && !s.prepareSync(u.sync) {
674		u.failed = true
675	}
676	if u.failed {
677		// Earlier commits still apply in order; only this update is discarded.
678		for len(s.queue) > 0 && s.queue[0] != u {
679			s.queue[0].applyGraph()
680		}
681		// Retire captured children too, cancelling their waits without replay.
682		u.dropFailedDeps()
683		for i, queued := range s.queue {
684			if queued == u {
685				s.dropQueuePrefix(i + 1)
686				break
687			}
688		}
689		return
690	}
691	for len(s.queue) > 0 && s.queue[0] != u {
692		s.queue[0].applyGraph()
693	}
694	for _, dep := range u.deps {
695		if !dep.owner.destroyed {
696			dep.applyGraph()
697		}
698	}
699	if len(s.queue) == 0 || s.queue[0] != u {
700		return
701	}
702	s.queue[0] = nil
703	if len(s.queue) == 1 {
704		s.queue = s.queue[:0] // retain capacity for the next commit
705	} else {
706		s.queue = s.queue[1:]
707	}
708	if len(s.queue) > 0 {
709		next := s.queue[0]
710		next.prev = nil
711		u.releaseRef() // next's prev no longer points to u
712	}
713	s.applyUpdate(u)
714	u.retireUpdate()
715}
716
717// dropFailedDeps retires just the child prefixes owned by a rejected commit.
718// Incoming graph refs retain their storage until the failed parent retires.
719func (u *update) dropFailedDeps() {
720	for _, dep := range u.deps {
721		if dep.retired {
722			continue
723		}
724		// Every predecessor in the retired prefix can own a different
725		// dependency graph. Traverse it before severing any incoming refs.
726		for _, prefix := range dep.owner.queue {
727			if !prefix.retired {
728				prefix.dropFailedDeps()
729			}
730			if prefix == dep {
731				break
732			}
733		}
734		for i, queued := range dep.owner.queue {
735			if queued == dep {
736				dep.owner.dropQueuePrefix(i + 1)
737				break
738			}
739		}
740	}
741}
742
743// sameBuffer reports whether two wrappers are the same wl_buffer.
744func sameBuffer(a, b *wayland.Buffer) bool {
745	return a != nil && b != nil && a.Resource == b.Resource
746}
747
748// wakePacer makes the pacer look at its queues now.
749func (s *Server) wakePacer() {
750	select {
751	case s.frameReady <- struct{}{}:
752	default:
753	}
754}

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 25

Tags 10

↑↓ moveenter openesc close