detached 3ed810f

fix(wayland): hold client buffers until every output moved past them

commit
3ed810f75981e851cf910faea390458f23751ada
author
brice · · 2026-09-25 17:00 UTC
parent
34cdd04
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +96 -45
M internal/adapters/drm/output.go +24 -3
@@ -37,6 +37,7 @@ type Output struct {
3737 clientFBs map[uint64]*clientFB
3838 shown, queued uint64
3939 reason string // why the last frame was composed ("" = scanout)
40+ unsent ports.OutputPresented
4041 }
4142
4243 // CursorLoader returns the image of a cursor at an output scale, at most
@@ -204,8 +205,17 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
204205 if flipped || reportDirty {
205206 o.report(presented, flipped, seen)
206207 flipped, reportDirty = false, false
208+ } else {
209+ o.flushReport(presented)
210+ }
211+ // An unsent report is retried soon, not only on the next event.
212+ var retry <-chan time.Time
213+ if o.unsent.Output != "" {
214+ retry = time.After(time.Millisecond)
207215 }
208216 select {
217+ case <-retry:
218+ continue
209219 case <-ctx.Done():
210220 return nil
211221 case on := <-active:
@@ -320,11 +330,22 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6
320330 // report tells wayland what the output shows and has read, so replaced
321331 // client buffers can be released. It must follow the frame decision: a
322332 // buffer is safe once a later content is seen and it is neither shown nor
323-// queued. A full channel drops the report; the next one carries the state.
333+// queued. A report the channel cannot take waits in unsent, replaced by
334+// newer ones (Flip kept), and is retried on the next loop.
324335 func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) {
325- r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}
336+ r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}
337+ o.unsent = r
338+ o.flushReport(presented)
339+}
340+
341+// flushReport sends the unsent report, if any, without blocking.
342+func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
343+ if o.unsent.Output == "" {
344+ return
345+ }
326346 select {
327- case presented <- r:
347+ case presented <- o.unsent:
348+ o.unsent = ports.OutputPresented{}
328349 default:
329350 }
330351 }
@@ -37,6 +37,7 @@ type Output struct {
37 clientFBs map[uint64]*clientFB37 clientFBs map[uint64]*clientFB
38 shown, queued uint6438 shown, queued uint64
39 reason string // why the last frame was composed ("" = scanout)39 reason string // why the last frame was composed ("" = scanout)
40+ unsent ports.OutputPresented
40 }41 }
41 42
42 // CursorLoader returns the image of a cursor at an output scale, at most43 // CursorLoader returns the image of a cursor at an output scale, at most
@@ -204,8 +205,17 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
204 if flipped || reportDirty {205 if flipped || reportDirty {
205 o.report(presented, flipped, seen)206 o.report(presented, flipped, seen)
206 flipped, reportDirty = false, false207 flipped, reportDirty = false, false
208+ } else {
209+ o.flushReport(presented)
210+ }
211+ // An unsent report is retried soon, not only on the next event.
212+ var retry <-chan time.Time
213+ if o.unsent.Output != "" {
214+ retry = time.After(time.Millisecond)
207 }215 }
208 select {216 select {
217+ case <-retry:
218+ continue
209 case <-ctx.Done():219 case <-ctx.Done():
210 return nil220 return nil
211 case on := <-active:221 case on := <-active:
@@ -320,11 +330,22 @@ func (o *Output) setCursor(load CursorLoader, c ports.CursorChange, scale float6
320 // report tells wayland what the output shows and has read, so replaced330 // report tells wayland what the output shows and has read, so replaced
321 // client buffers can be released. It must follow the frame decision: a331 // client buffers can be released. It must follow the frame decision: a
322 // buffer is safe once a later content is seen and it is neither shown nor332 // buffer is safe once a later content is seen and it is neither shown nor
323-// queued. A full channel drops the report; the next one carries the state.333+// queued. A report the channel cannot take waits in unsent, replaced by
334+// newer ones (Flip kept), and is retried on the next loop.
324 func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) {335 func (o *Output) report(presented chan<- ports.OutputPresented, flip bool, seen map[ports.WindowID]uint64) {
325- r := ports.OutputPresented{Output: o.conn.name, Flip: flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}336+ r := ports.OutputPresented{Output: o.conn.name, Flip: flip || o.unsent.Flip, Shown: o.shown, Queued: o.queued, Seen: maps.Clone(seen)}
337+ o.unsent = r
338+ o.flushReport(presented)
339+}
340+
341+// flushReport sends the unsent report, if any, without blocking.
342+func (o *Output) flushReport(presented chan<- ports.OutputPresented) {
343+ if o.unsent.Output == "" {
344+ return
345+ }
326 select {346 select {
327- case presented <- r:347+ case presented <- o.unsent:
348+ o.unsent = ports.OutputPresented{}
328 default:349 default:
329 }350 }
330 }351 }
M internal/adapters/headless/output.go +23 -7
@@ -42,6 +42,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
4242 var want ports.CursorChange
4343 cursorScale := -1.0 // not loaded yet
4444 seen := map[ports.WindowID]uint64{}
45+ // pending is a report the channel could not take, retried soon.
46+ var pending *ports.OutputPresented
4547 update := func(c ports.SurfaceContent) {
4648 seen[c.ID] = max(seen[c.ID], c.Seq)
4749 dirty = dirty || scene.Shows(c.ID)
@@ -55,9 +57,16 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
5557 if ctx.Err() != nil {
5658 return nil
5759 }
60+ var retry <-chan time.Time
61+ if pending != nil {
62+ retry = time.After(time.Millisecond)
63+ }
5864 select {
5965 case <-ctx.Done():
6066 return nil
67+ case <-retry:
68+ pending = opts.send(pending)
69+ continue
6170 case s, ok := <-scenes:
6271 if !ok {
6372 scenes = nil
@@ -96,7 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
96105 }
97106 if !haveScene || !dirty {
98107 // Contents not drawn are still read: report them.
99- opts.report(false, seen)
108+ pending = opts.report(pending, seen)
100109 continue
101110 }
102111 dirty = false
@@ -113,7 +122,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
113122 return fmt.Errorf("render frame: %w", err)
114123 }
115124 frame++
116- opts.report(false, seen)
125+ pending = opts.report(pending, seen)
117126 if opts.ScreenshotDir != "" {
118127 shot := r.Pixels()
119128 if opts.Cursor != nil {
@@ -147,14 +156,21 @@ func writePNG(path string, img *image.RGBA) error {
147156 }
148157
149158 // report sends what the output read; a full channel drops it.
150-// Headless frames are not paced by a screen: Flip stays false and frame
151-// callbacks follow the refresh timer.
152-func (opts Options) report(flip bool, seen map[ports.WindowID]uint64) {
159+// report sends what the output read, or returns it to retry when the
160+// channel is full. Headless frames are not paced by a screen: Flip stays
161+// false and frame callbacks follow the refresh timer.
162+func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented {
153163 if opts.Presented == nil {
154- return
164+ return nil
155165 }
166+ return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)})
167+}
168+
169+func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented {
156170 select {
157- case opts.Presented <- ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: maps.Clone(seen)}:
171+ case opts.Presented <- *r:
172+ return nil
158173 default:
174+ return r
159175 }
160176 }
@@ -42,6 +42,8 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
42 var want ports.CursorChange42 var want ports.CursorChange
43 cursorScale := -1.0 // not loaded yet43 cursorScale := -1.0 // not loaded yet
44 seen := map[ports.WindowID]uint64{}44 seen := map[ports.WindowID]uint64{}
45+ // pending is a report the channel could not take, retried soon.
46+ var pending *ports.OutputPresented
45 update := func(c ports.SurfaceContent) {47 update := func(c ports.SurfaceContent) {
46 seen[c.ID] = max(seen[c.ID], c.Seq)48 seen[c.ID] = max(seen[c.ID], c.Seq)
47 dirty = dirty || scene.Shows(c.ID)49 dirty = dirty || scene.Shows(c.ID)
@@ -55,9 +57,16 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
55 if ctx.Err() != nil {57 if ctx.Err() != nil {
56 return nil58 return nil
57 }59 }
60+ var retry <-chan time.Time
61+ if pending != nil {
62+ retry = time.After(time.Millisecond)
63+ }
58 select {64 select {
59 case <-ctx.Done():65 case <-ctx.Done():
60 return nil66 return nil
67+ case <-retry:
68+ pending = opts.send(pending)
69+ continue
61 case s, ok := <-scenes:70 case s, ok := <-scenes:
62 if !ok {71 if !ok {
63 scenes = nil72 scenes = nil
@@ -96,7 +105,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
96 }105 }
97 if !haveScene || !dirty {106 if !haveScene || !dirty {
98 // Contents not drawn are still read: report them.107 // Contents not drawn are still read: report them.
99- opts.report(false, seen)108+ pending = opts.report(pending, seen)
100 continue109 continue
101 }110 }
102 dirty = false111 dirty = false
@@ -113,7 +122,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
113 return fmt.Errorf("render frame: %w", err)122 return fmt.Errorf("render frame: %w", err)
114 }123 }
115 frame++124 frame++
116- opts.report(false, seen)125+ pending = opts.report(pending, seen)
117 if opts.ScreenshotDir != "" {126 if opts.ScreenshotDir != "" {
118 shot := r.Pixels()127 shot := r.Pixels()
119 if opts.Cursor != nil {128 if opts.Cursor != nil {
@@ -147,14 +156,21 @@ func writePNG(path string, img *image.RGBA) error {
147 }156 }
148 157
149 // report sends what the output read; a full channel drops it.158 // report sends what the output read; a full channel drops it.
150-// Headless frames are not paced by a screen: Flip stays false and frame159+// report sends what the output read, or returns it to retry when the
151-// callbacks follow the refresh timer.160+// channel is full. Headless frames are not paced by a screen: Flip stays
152-func (opts Options) report(flip bool, seen map[ports.WindowID]uint64) {161+// false and frame callbacks follow the refresh timer.
162+func (opts Options) report(_ *ports.OutputPresented, seen map[ports.WindowID]uint64) *ports.OutputPresented {
153 if opts.Presented == nil {163 if opts.Presented == nil {
154- return164+ return nil
155 }165 }
166+ return opts.send(&ports.OutputPresented{Output: opts.Name, Seen: maps.Clone(seen)})
167+}
168+
169+func (opts Options) send(r *ports.OutputPresented) *ports.OutputPresented {
156 select {170 select {
157- case opts.Presented <- ports.OutputPresented{Output: opts.Name, Flip: flip, Seen: maps.Clone(seen)}:171+ case opts.Presented <- *r:
172+ return nil
158 default:173 default:
174+ return r
159 }175 }
160 }176 }
M internal/adapters/wayland/release.go +26 -19
@@ -23,7 +23,6 @@ const heldTimeout = 100 * time.Millisecond
2323 type heldBuffer struct {
2424 res *wayland.Buffer
2525 window ports.WindowID
26- output string
2726 id uint64 // DMABuf ID, 0 for wl_shm
2827 after uint64 // the window's content Seq when the buffer was replaced
2928 at time.Time
@@ -41,7 +40,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
4140 b.SendRelease()
4241 return
4342 }
44- h := heldBuffer{res: b, window: window, output: name, at: time.Now()}
43+ h := heldBuffer{res: b, window: window, at: time.Now()}
4544 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {
4645 h.id = d.buf.ID
4746 }
@@ -57,24 +56,26 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
5756 }
5857
5958 // releaseHeld records the output reports and releases the buffers no
60-// output reads. It reports whether buffers are still held.
59+// output reads. Every output reads every window's content, so a buffer
60+// waits until no live output shows or queues it and all of them have seen
61+// a later content of its window (or heldTimeout passed). It reports
62+// whether buffers are still held.
6163 func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool {
6264 for _, r := range reports {
6365 s.reports[r.Output] = r
6466 }
65- live := map[string]bool{}
66- for _, o := range s.outputs {
67- live[o.name()] = true
68- }
67+ live := make([]ports.OutputPresented, 0, len(s.outputs))
6968 for name := range s.reports {
70- if !live[name] {
69+ if s.outputByNameExact(name) == nil {
7170 delete(s.reports, name)
7271 }
7372 }
73+ for _, r := range s.reports {
74+ live = append(live, r)
75+ }
7476 kept := s.held[:0]
7577 for _, h := range s.held {
76- r, reported := s.reports[h.output]
77- if live[h.output] && keepHeld(h, r, reported, now) {
78+ if keepHeld(h, live, now) {
7879 kept = append(kept, h)
7980 continue
8081 }
@@ -87,15 +88,21 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
8788 return len(s.held) > 0
8889 }
8990
90-// keepHeld reports whether the output (last report r) may still read a
91-// held buffer: it scans it out or queued it, it has not got a later
92-// content of the window yet, or it has not reported within heldTimeout.
93-func keepHeld(h heldBuffer, r ports.OutputPresented, reported bool, now time.Time) bool {
94- if reported && h.id != 0 && (r.Shown == h.id || r.Queued == h.id) {
95- return true
91+// keepHeld reports whether an output may still read a held buffer: one
92+// scans it out or queued it, or one has not got a later content of its
93+// window yet and heldTimeout has not passed.
94+func keepHeld(h heldBuffer, reports []ports.OutputPresented, now time.Time) bool {
95+ seen := true
96+ for _, r := range reports {
97+ if h.id != 0 && (r.Shown == h.id || r.Queued == h.id) {
98+ return true
99+ }
100+ if r.Seen[h.window] <= h.after {
101+ seen = false
102+ }
96103 }
97- if reported && r.Seen[h.window] > h.after {
98- return false
104+ if len(reports) == 0 {
105+ seen = false
99106 }
100- return now.Sub(h.at) < heldTimeout
107+ return !seen && now.Sub(h.at) < heldTimeout
101108 }
@@ -23,7 +23,6 @@ const heldTimeout = 100 * time.Millisecond
23 type heldBuffer struct {23 type heldBuffer struct {
24 res *wayland.Buffer24 res *wayland.Buffer
25 window ports.WindowID25 window ports.WindowID
26- output string
27 id uint64 // DMABuf ID, 0 for wl_shm26 id uint64 // DMABuf ID, 0 for wl_shm
28 after uint64 // the window's content Seq when the buffer was replaced27 after uint64 // the window's content Seq when the buffer was replaced
29 at time.Time28 at time.Time
@@ -41,7 +40,7 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
41 b.SendRelease()40 b.SendRelease()
42 return41 return
43 }42 }
44- h := heldBuffer{res: b, window: window, output: name, at: time.Now()}43+ h := heldBuffer{res: b, window: window, at: time.Now()}
45 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {44 if d, ok := s.buffers[b.Resource].(*dmabufBuffer); ok {
46 h.id = d.buf.ID45 h.id = d.buf.ID
47 }46 }
@@ -57,24 +56,26 @@ func (s *Server) releaseBuffer(surf *surface, b *wayland.Buffer) {
57 }56 }
58 57
59 // releaseHeld records the output reports and releases the buffers no58 // releaseHeld records the output reports and releases the buffers no
60-// output reads. It reports whether buffers are still held.59+// output reads. Every output reads every window's content, so a buffer
60+// waits until no live output shows or queues it and all of them have seen
61+// a later content of its window (or heldTimeout passed). It reports
62+// whether buffers are still held.
61 func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool {63 func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) bool {
62 for _, r := range reports {64 for _, r := range reports {
63 s.reports[r.Output] = r65 s.reports[r.Output] = r
64 }66 }
65- live := map[string]bool{}67+ live := make([]ports.OutputPresented, 0, len(s.outputs))
66- for _, o := range s.outputs {
67- live[o.name()] = true
68- }
69 for name := range s.reports {68 for name := range s.reports {
70- if !live[name] {69+ if s.outputByNameExact(name) == nil {
71 delete(s.reports, name)70 delete(s.reports, name)
72 }71 }
73 }72 }
73+ for _, r := range s.reports {
74+ live = append(live, r)
75+ }
74 kept := s.held[:0]76 kept := s.held[:0]
75 for _, h := range s.held {77 for _, h := range s.held {
76- r, reported := s.reports[h.output]78+ if keepHeld(h, live, now) {
77- if live[h.output] && keepHeld(h, r, reported, now) {
78 kept = append(kept, h)79 kept = append(kept, h)
79 continue80 continue
80 }81 }
@@ -87,15 +88,21 @@ func (s *Server) releaseHeld(now time.Time, reports []ports.OutputPresented) boo
87 return len(s.held) > 088 return len(s.held) > 0
88 }89 }
89 90
90-// keepHeld reports whether the output (last report r) may still read a91+// keepHeld reports whether an output may still read a held buffer: one
91-// held buffer: it scans it out or queued it, it has not got a later92+// scans it out or queued it, or one has not got a later content of its
92-// content of the window yet, or it has not reported within heldTimeout.93+// window yet and heldTimeout has not passed.
93-func keepHeld(h heldBuffer, r ports.OutputPresented, reported bool, now time.Time) bool {94+func keepHeld(h heldBuffer, reports []ports.OutputPresented, now time.Time) bool {
94- if reported && h.id != 0 && (r.Shown == h.id || r.Queued == h.id) {95+ seen := true
95- return true96+ for _, r := range reports {
97+ if h.id != 0 && (r.Shown == h.id || r.Queued == h.id) {
98+ return true
99+ }
100+ if r.Seen[h.window] <= h.after {
101+ seen = false
102+ }
96 }103 }
97- if reported && r.Seen[h.window] > h.after {104+ if len(reports) == 0 {
98- return false105+ seen = false
99 }106 }
100- return now.Sub(h.at) < heldTimeout107+ return !seen && now.Sub(h.at) < heldTimeout
101 }108 }
M internal/adapters/wayland/release_test.go +23 -16
@@ -11,25 +11,32 @@ func TestKeepHeld(t *testing.T) {
1111 now := time.Unix(100, 0)
1212 dma := heldBuffer{window: 1, id: 7, after: 5, at: now}
1313 shm := heldBuffer{window: 1, after: 5, at: now}
14- seen := func(seq uint64) map[ports.WindowID]uint64 { return map[ports.WindowID]uint64{1: seq} }
14+ out := func(shown, queued, seq uint64) ports.OutputPresented {
15+ return ports.OutputPresented{Shown: shown, Queued: queued, Seen: map[ports.WindowID]uint64{1: seq}}
16+ }
17+ type R = []ports.OutputPresented
1518 for _, tc := range []struct {
16- name string
17- h heldBuffer
18- r ports.OutputPresented
19- reported bool
20- after time.Duration
21- want bool
19+ name string
20+ h heldBuffer
21+ reports R
22+ after time.Duration
23+ want bool
2224 }{
23- {"no report yet", dma, ports.OutputPresented{}, false, 0, true},
24- {"output has only this content", dma, ports.OutputPresented{Seen: seen(5)}, true, 0, true},
25- {"later content seen", dma, ports.OutputPresented{Seen: seen(6)}, true, 0, false},
26- {"scanned out", dma, ports.OutputPresented{Shown: 7, Seen: seen(6)}, true, time.Second, true},
27- {"queued for scanout", dma, ports.OutputPresented{Queued: 7, Seen: seen(6)}, true, time.Second, true},
28- {"another buffer scanned out", dma, ports.OutputPresented{Shown: 8, Seen: seen(6)}, true, 0, false},
29- {"silent output times out", dma, ports.OutputPresented{Seen: seen(5)}, true, heldTimeout, false},
30- {"shm follows content", shm, ports.OutputPresented{Shown: 0, Seen: seen(6)}, true, 0, false},
25+ {"no report yet", dma, nil, 0, true},
26+ {"no report times out", dma, nil, heldTimeout, false},
27+ {"output has only this content", dma, R{out(0, 0, 5)}, 0, true},
28+ {"later content seen", dma, R{out(0, 0, 6)}, 0, false},
29+ {"scanned out", dma, R{out(7, 0, 6)}, time.Second, true},
30+ {"queued for scanout", dma, R{out(0, 7, 6)}, time.Second, true},
31+ {"another buffer scanned out", dma, R{out(8, 0, 6)}, 0, false},
32+ {"silent output times out", dma, R{out(0, 0, 5)}, heldTimeout, false},
33+ {"shm follows content", shm, R{out(0, 0, 6)}, 0, false},
34+ // A window moved off output A: A still scans the buffer out.
35+ {"other output scans it out", dma, R{out(0, 0, 6), out(7, 0, 6)}, time.Second, true},
36+ {"other output lags", dma, R{out(0, 0, 6), out(0, 0, 5)}, 0, true},
37+ {"all outputs moved on", dma, R{out(0, 0, 6), out(0, 0, 6)}, 0, false},
3138 } {
32- if got := keepHeld(tc.h, tc.r, tc.reported, now.Add(tc.after)); got != tc.want {
39+ if got := keepHeld(tc.h, tc.reports, now.Add(tc.after)); got != tc.want {
3340 t.Errorf("%s: %v", tc.name, got)
3441 }
3542 }
@@ -11,25 +11,32 @@ func TestKeepHeld(t *testing.T) {
11 now := time.Unix(100, 0)11 now := time.Unix(100, 0)
12 dma := heldBuffer{window: 1, id: 7, after: 5, at: now}12 dma := heldBuffer{window: 1, id: 7, after: 5, at: now}
13 shm := heldBuffer{window: 1, after: 5, at: now}13 shm := heldBuffer{window: 1, after: 5, at: now}
14- seen := func(seq uint64) map[ports.WindowID]uint64 { return map[ports.WindowID]uint64{1: seq} }14+ out := func(shown, queued, seq uint64) ports.OutputPresented {
15+ return ports.OutputPresented{Shown: shown, Queued: queued, Seen: map[ports.WindowID]uint64{1: seq}}
16+ }
17+ type R = []ports.OutputPresented
15 for _, tc := range []struct {18 for _, tc := range []struct {
16- name string19+ name string
17- h heldBuffer20+ h heldBuffer
18- r ports.OutputPresented21+ reports R
19- reported bool22+ after time.Duration
20- after time.Duration23+ want bool
21- want bool
22 }{24 }{
23- {"no report yet", dma, ports.OutputPresented{}, false, 0, true},25+ {"no report yet", dma, nil, 0, true},
24- {"output has only this content", dma, ports.OutputPresented{Seen: seen(5)}, true, 0, true},26+ {"no report times out", dma, nil, heldTimeout, false},
25- {"later content seen", dma, ports.OutputPresented{Seen: seen(6)}, true, 0, false},27+ {"output has only this content", dma, R{out(0, 0, 5)}, 0, true},
26- {"scanned out", dma, ports.OutputPresented{Shown: 7, Seen: seen(6)}, true, time.Second, true},28+ {"later content seen", dma, R{out(0, 0, 6)}, 0, false},
27- {"queued for scanout", dma, ports.OutputPresented{Queued: 7, Seen: seen(6)}, true, time.Second, true},29+ {"scanned out", dma, R{out(7, 0, 6)}, time.Second, true},
28- {"another buffer scanned out", dma, ports.OutputPresented{Shown: 8, Seen: seen(6)}, true, 0, false},30+ {"queued for scanout", dma, R{out(0, 7, 6)}, time.Second, true},
29- {"silent output times out", dma, ports.OutputPresented{Seen: seen(5)}, true, heldTimeout, false},31+ {"another buffer scanned out", dma, R{out(8, 0, 6)}, 0, false},
30- {"shm follows content", shm, ports.OutputPresented{Shown: 0, Seen: seen(6)}, true, 0, false},32+ {"silent output times out", dma, R{out(0, 0, 5)}, heldTimeout, false},
33+ {"shm follows content", shm, R{out(0, 0, 6)}, 0, false},
34+ // A window moved off output A: A still scans the buffer out.
35+ {"other output scans it out", dma, R{out(0, 0, 6), out(7, 0, 6)}, time.Second, true},
36+ {"other output lags", dma, R{out(0, 0, 6), out(0, 0, 5)}, 0, true},
37+ {"all outputs moved on", dma, R{out(0, 0, 6), out(0, 0, 6)}, 0, false},
31 } {38 } {
32- if got := keepHeld(tc.h, tc.r, tc.reported, now.Add(tc.after)); got != tc.want {39+ if got := keepHeld(tc.h, tc.reports, now.Add(tc.after)); got != tc.want {
33 t.Errorf("%s: %v", tc.name, got)40 t.Errorf("%s: %v", tc.name, got)
34 }41 }
35 }42 }

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 23

Tags 10

↑↓ moveenter openesc close