detached 37a29ce

feat(wayland): pace frame callbacks on each output's page flips and refresh

commit
37a29ce2b5471130787a05a744480b7f897188a4
author
brice · · 2026-09-25 15:13 UTC
parent
59256c2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +188 -48
M internal/adapters/drm/output.go +6 -1
@@ -117,7 +117,8 @@ func (o *Output) Close() {
117117 }
118118
119119 // Run renders scenes and flips until ctx ends. active reports seat enable/disable.
120-func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error {
120+// Each completed page flip is reported on presented (dropped when full).
121+func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, presented chan<- ports.OutputPresented) error {
121122 r, err := newRenderer(o.Width(), o.Height())
122123 if err != nil {
123124 return fmt.Errorf("create renderer: %w", err)
@@ -169,6 +170,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
169170 }
170171 o.pending = false
171172 o.flips += n
173+ select {
174+ case presented <- ports.OutputPresented{Output: o.conn.name}:
175+ default:
176+ }
172177 if d := time.Since(o.flipStart); d > 20*time.Millisecond {
173178 o.log.Info().Dur("flip_ms", d).Msg("slow flip")
174179 }
@@ -117,7 +117,8 @@ func (o *Output) Close() {
117 }117 }
118 118
119 // Run renders scenes and flips until ctx ends. active reports seat enable/disable.119 // Run renders scenes and flips until ctx ends. active reports seat enable/disable.
120-func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange) error {120+// Each completed page flip is reported on presented (dropped when full).
121+func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Renderer, error), loadCursor CursorLoader, active <-chan bool, scenes <-chan ports.Scene, contents <-chan ports.SurfaceContent, cursor <-chan ports.CursorChange, presented chan<- ports.OutputPresented) error {
121 r, err := newRenderer(o.Width(), o.Height())122 r, err := newRenderer(o.Width(), o.Height())
122 if err != nil {123 if err != nil {
123 return fmt.Errorf("create renderer: %w", err)124 return fmt.Errorf("create renderer: %w", err)
@@ -169,6 +170,10 @@ func (o *Output) Run(ctx context.Context, newRenderer func(w, h int) (ports.Rend
169 }170 }
170 o.pending = false171 o.pending = false
171 o.flips += n172 o.flips += n
173+ select {
174+ case presented <- ports.OutputPresented{Output: o.conn.name}:
175+ default:
176+ }
172 if d := time.Since(o.flipStart); d > 20*time.Millisecond {177 if d := time.Since(o.flipStart); d > 20*time.Millisecond {
173 o.log.Info().Dur("flip_ms", d).Msg("slow flip")178 o.log.Info().Dur("flip_ms", d).Msg("slow flip")
174 }179 }
A internal/adapters/wayland/frames.go +106 -0
@@ -0,0 +1,106 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "time"
6+)
7+
8+// Frame callbacks (wl_surface.frame) tell a client when to draw its next
9+// frame. They are paced per output: on its page flip when it flips, else
10+// at its refresh rate, so a 165 Hz output lets clients draw 165 frames a
11+// second and a V-Sync game follows the screen.
12+
13+// defaultFramePeriod paces surfaces without an output (60 Hz).
14+const defaultFramePeriod = time.Second / 60
15+
16+// pace fires the due frame callbacks on each page flip and timer tick.
17+func (s *Server) pace(ctx context.Context) {
18+ timer := time.NewTimer(defaultFramePeriod)
19+ defer timer.Stop()
20+ presented := s.channels.Presented
21+ for {
22+ flipped, flip := "", false
23+ select {
24+ case <-ctx.Done():
25+ return
26+ case <-s.display.Stopped():
27+ return
28+ case p := <-presented:
29+ flipped, flip = p.Output, true
30+ case <-timer.C:
31+ }
32+ wait := defaultFramePeriod
33+ if !s.display.Do(func() {
34+ if ctx.Err() == nil {
35+ wait = s.fireFrames(time.Now(), flipped, flip)
36+ }
37+ }) {
38+ return
39+ }
40+ timer.Reset(wait)
41+ }
42+}
43+
44+// fireFrames sends the frame callbacks due at now (all those of a flipped
45+// output) and returns the time to the next deadline.
46+func (s *Server) fireFrames(now time.Time, flipped string, flip bool) time.Duration {
47+ periods := make(map[string]time.Duration, len(s.outputs))
48+ for _, o := range s.outputs {
49+ periods[o.name()] = framePeriod(o.place.Info.RefreshMilli)
50+ }
51+ if flip {
52+ if p, ok := periods[flipped]; ok {
53+ s.sendFrames(flipped)
54+ // A flipping output is paced by its flips; the timer only
55+ // catches a missed one.
56+ s.frameDue[flipped] = now.Add(p + p/2)
57+ }
58+ }
59+ for name := range s.frameDue {
60+ if _, ok := periods[name]; !ok {
61+ delete(s.frameDue, name)
62+ }
63+ }
64+ for name, p := range periods {
65+ if due, ok := s.frameDue[name]; !ok || !now.Before(due) {
66+ s.sendFrames(name)
67+ s.frameDue[name] = now.Add(p)
68+ }
69+ }
70+ // Callbacks of surfaces without a known output go at the default rate.
71+ for name := range s.awaiting {
72+ if _, ok := periods[name]; !ok {
73+ s.sendFrames(name)
74+ }
75+ }
76+ wait := defaultFramePeriod
77+ for _, due := range s.frameDue {
78+ wait = min(wait, due.Sub(now))
79+ }
80+ return max(wait, time.Millisecond)
81+}
82+
83+func (s *Server) sendFrames(name string) {
84+ callbacks := s.awaiting[name]
85+ if len(callbacks) == 0 {
86+ return
87+ }
88+ delete(s.awaiting, name)
89+ ms := uint32(time.Since(s.started).Milliseconds())
90+ for _, cb := range callbacks {
91+ if !cb.Resource.Alive() {
92+ continue
93+ }
94+ cb.SendDone(ms)
95+ cb.Destroy()
96+ s.frames++
97+ }
98+}
99+
100+// framePeriod is one refresh at refreshMilli mHz, 60 Hz when unknown.
101+func framePeriod(refreshMilli int) time.Duration {
102+ if refreshMilli <= 0 {
103+ return defaultFramePeriod
104+ }
105+ return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli))
106+}
@@ -0,0 +1,106 @@
1+package wayland
2+
3+import (
4+ "context"
5+ "time"
6+)
7+
8+// Frame callbacks (wl_surface.frame) tell a client when to draw its next
9+// frame. They are paced per output: on its page flip when it flips, else
10+// at its refresh rate, so a 165 Hz output lets clients draw 165 frames a
11+// second and a V-Sync game follows the screen.
12+
13+// defaultFramePeriod paces surfaces without an output (60 Hz).
14+const defaultFramePeriod = time.Second / 60
15+
16+// pace fires the due frame callbacks on each page flip and timer tick.
17+func (s *Server) pace(ctx context.Context) {
18+ timer := time.NewTimer(defaultFramePeriod)
19+ defer timer.Stop()
20+ presented := s.channels.Presented
21+ for {
22+ flipped, flip := "", false
23+ select {
24+ case <-ctx.Done():
25+ return
26+ case <-s.display.Stopped():
27+ return
28+ case p := <-presented:
29+ flipped, flip = p.Output, true
30+ case <-timer.C:
31+ }
32+ wait := defaultFramePeriod
33+ if !s.display.Do(func() {
34+ if ctx.Err() == nil {
35+ wait = s.fireFrames(time.Now(), flipped, flip)
36+ }
37+ }) {
38+ return
39+ }
40+ timer.Reset(wait)
41+ }
42+}
43+
44+// fireFrames sends the frame callbacks due at now (all those of a flipped
45+// output) and returns the time to the next deadline.
46+func (s *Server) fireFrames(now time.Time, flipped string, flip bool) time.Duration {
47+ periods := make(map[string]time.Duration, len(s.outputs))
48+ for _, o := range s.outputs {
49+ periods[o.name()] = framePeriod(o.place.Info.RefreshMilli)
50+ }
51+ if flip {
52+ if p, ok := periods[flipped]; ok {
53+ s.sendFrames(flipped)
54+ // A flipping output is paced by its flips; the timer only
55+ // catches a missed one.
56+ s.frameDue[flipped] = now.Add(p + p/2)
57+ }
58+ }
59+ for name := range s.frameDue {
60+ if _, ok := periods[name]; !ok {
61+ delete(s.frameDue, name)
62+ }
63+ }
64+ for name, p := range periods {
65+ if due, ok := s.frameDue[name]; !ok || !now.Before(due) {
66+ s.sendFrames(name)
67+ s.frameDue[name] = now.Add(p)
68+ }
69+ }
70+ // Callbacks of surfaces without a known output go at the default rate.
71+ for name := range s.awaiting {
72+ if _, ok := periods[name]; !ok {
73+ s.sendFrames(name)
74+ }
75+ }
76+ wait := defaultFramePeriod
77+ for _, due := range s.frameDue {
78+ wait = min(wait, due.Sub(now))
79+ }
80+ return max(wait, time.Millisecond)
81+}
82+
83+func (s *Server) sendFrames(name string) {
84+ callbacks := s.awaiting[name]
85+ if len(callbacks) == 0 {
86+ return
87+ }
88+ delete(s.awaiting, name)
89+ ms := uint32(time.Since(s.started).Milliseconds())
90+ for _, cb := range callbacks {
91+ if !cb.Resource.Alive() {
92+ continue
93+ }
94+ cb.SendDone(ms)
95+ cb.Destroy()
96+ s.frames++
97+ }
98+}
99+
100+// framePeriod is one refresh at refreshMilli mHz, 60 Hz when unknown.
101+func framePeriod(refreshMilli int) time.Duration {
102+ if refreshMilli <= 0 {
103+ return defaultFramePeriod
104+ }
105+ return time.Duration(int64(time.Second) * 1000 / int64(refreshMilli))
106+}
A internal/adapters/wayland/frames_test.go +45 -0
@@ -0,0 +1,45 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+)
9+
10+func TestFramePeriod(t *testing.T) {
11+ for _, tc := range []struct {
12+ milli int
13+ want time.Duration
14+ }{{0, time.Second / 60}, {60000, time.Second / 60}, {165058, 6058476}, {144000, 6944444}} {
15+ if got := framePeriod(tc.milli); got != tc.want {
16+ t.Errorf("%d: %v want %v", tc.milli, got, tc.want)
17+ }
18+ }
19+}
20+
21+// Each output is paced at its own refresh; a page flip resets its deadline.
22+func TestFireFramesPacing(t *testing.T) {
23+ fast := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 165000}}}
24+ slow := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "HDMI-A-1", RefreshMilli: 60000}}}
25+ s := &Server{outputs: []*output{fast, slow}, frameDue: map[string]time.Time{}}
26+ now := time.Unix(100, 0)
27+ if wait := s.fireFrames(now, "", false); wait != framePeriod(165000) {
28+ t.Fatalf("first wait %v", wait)
29+ }
30+ if due := s.frameDue["HDMI-A-1"]; due != now.Add(time.Second/60) {
31+ t.Fatalf("60 Hz due %v", due.Sub(now))
32+ }
33+ // A flip on DP-2 pushes its timer back: flips pace it.
34+ later := now.Add(time.Millisecond)
35+ s.fireFrames(later, "DP-2", true)
36+ if due := s.frameDue["DP-2"]; due != later.Add(framePeriod(165000)*3/2) {
37+ t.Fatalf("flip due %v", due.Sub(later))
38+ }
39+ // An unplugged output is forgotten.
40+ s.outputs = []*output{fast}
41+ s.fireFrames(later, "", false)
42+ if _, ok := s.frameDue["HDMI-A-1"]; ok {
43+ t.Fatal("stale output kept")
44+ }
45+}
@@ -0,0 +1,45 @@
1+package wayland
2+
3+import (
4+ "testing"
5+ "time"
6+
7+ "github.com/bnema/nefertty/internal/ports"
8+)
9+
10+func TestFramePeriod(t *testing.T) {
11+ for _, tc := range []struct {
12+ milli int
13+ want time.Duration
14+ }{{0, time.Second / 60}, {60000, time.Second / 60}, {165058, 6058476}, {144000, 6944444}} {
15+ if got := framePeriod(tc.milli); got != tc.want {
16+ t.Errorf("%d: %v want %v", tc.milli, got, tc.want)
17+ }
18+ }
19+}
20+
21+// Each output is paced at its own refresh; a page flip resets its deadline.
22+func TestFireFramesPacing(t *testing.T) {
23+ fast := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 165000}}}
24+ slow := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "HDMI-A-1", RefreshMilli: 60000}}}
25+ s := &Server{outputs: []*output{fast, slow}, frameDue: map[string]time.Time{}}
26+ now := time.Unix(100, 0)
27+ if wait := s.fireFrames(now, "", false); wait != framePeriod(165000) {
28+ t.Fatalf("first wait %v", wait)
29+ }
30+ if due := s.frameDue["HDMI-A-1"]; due != now.Add(time.Second/60) {
31+ t.Fatalf("60 Hz due %v", due.Sub(now))
32+ }
33+ // A flip on DP-2 pushes its timer back: flips pace it.
34+ later := now.Add(time.Millisecond)
35+ s.fireFrames(later, "DP-2", true)
36+ if due := s.frameDue["DP-2"]; due != later.Add(framePeriod(165000)*3/2) {
37+ t.Fatalf("flip due %v", due.Sub(later))
38+ }
39+ // An unplugged output is forgotten.
40+ s.outputs = []*output{fast}
41+ s.fireFrames(later, "", false)
42+ if _, ok := s.frameDue["HDMI-A-1"]; ok {
43+ t.Fatal("stale output kept")
44+ }
45+}
M internal/adapters/wayland/server.go +15 -42
@@ -35,6 +35,9 @@ type Channels struct {
3535 Commands <-chan ports.ClientCommand
3636 // Cursors receives the cursor the client under the pointer asks for.
3737 Cursors chan<- ports.CursorChange
38+ // Presented paces frame callbacks on the outputs' page flips; outputs
39+ // that do not flip (idle, headless) are paced at their refresh rate.
40+ Presented <-chan ports.OutputPresented
3841 }
3942 type Server struct {
4043 display *server.Display
@@ -44,13 +47,16 @@ type Server struct {
4447 cleanup func()
4548 log zerowrap.Logger
4649 channels Channels
47- awaiting []*wayland.Callback
48- frames uint64
49- started time.Time
50- surfaces map[*server.Resource]*surface
51- buffers map[*server.Resource]clientBuffer
52- dmabuf *dmabufGlobal
53- serial uint32
50+ // awaiting holds frame callbacks by output name, due at its next
51+ // frame (frameDue, or its page flip).
52+ awaiting map[string][]*wayland.Callback
53+ frameDue map[string]time.Time
54+ frames uint64
55+ started time.Time
56+ surfaces map[*server.Resource]*surface
57+ buffers map[*server.Resource]clientBuffer
58+ dmabuf *dmabufGlobal
59+ serial uint32
5460 // press is the serial of the last button or key press, sent to
5561 // pressClient: popup grabs must come from it.
5662 press uint32
@@ -136,7 +142,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
136142 d.Close()
137143 return nil, err
138144 }
139- s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
145+ s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
140146 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
141147 if opts.Keymap != "" {
142148 fd, size, e := keymapFile(opts.Keymap)
@@ -207,40 +213,7 @@ func (s *Server) Run(ctx context.Context) error {
207213 }
208214 }
209215 }()
210- go func() {
211- defer wg.Done()
212- ticker := time.NewTicker(16 * time.Millisecond)
213- defer ticker.Stop()
214- for {
215- select {
216- case <-ctx.Done():
217- return
218- case <-s.display.Stopped():
219- return
220- case <-ticker.C:
221- if ctx.Err() != nil {
222- return
223- }
224- if !s.display.Do(func() {
225- if ctx.Err() != nil {
226- return
227- }
228- callbacks := s.awaiting
229- s.awaiting = nil
230- for _, cb := range callbacks {
231- if !cb.Resource.Alive() {
232- continue
233- }
234- cb.SendDone(uint32(time.Since(s.started).Milliseconds()))
235- cb.Destroy()
236- s.frames++
237- }
238- }) {
239- return
240- }
241- }
242- }
243- }()
216+ go func() { defer wg.Done(); s.pace(ctx) }()
244217 err := s.display.Run(ctx)
245218 wg.Wait()
246219 return err
@@ -35,6 +35,9 @@ type Channels struct {
35 Commands <-chan ports.ClientCommand35 Commands <-chan ports.ClientCommand
36 // Cursors receives the cursor the client under the pointer asks for.36 // Cursors receives the cursor the client under the pointer asks for.
37 Cursors chan<- ports.CursorChange37 Cursors chan<- ports.CursorChange
38+ // Presented paces frame callbacks on the outputs' page flips; outputs
39+ // that do not flip (idle, headless) are paced at their refresh rate.
40+ Presented <-chan ports.OutputPresented
38 }41 }
39 type Server struct {42 type Server struct {
40 display *server.Display43 display *server.Display
@@ -44,13 +47,16 @@ type Server struct {
44 cleanup func()47 cleanup func()
45 log zerowrap.Logger48 log zerowrap.Logger
46 channels Channels49 channels Channels
47- awaiting []*wayland.Callback50+ // awaiting holds frame callbacks by output name, due at its next
48- frames uint6451+ // frame (frameDue, or its page flip).
49- started time.Time52+ awaiting map[string][]*wayland.Callback
50- surfaces map[*server.Resource]*surface53+ frameDue map[string]time.Time
51- buffers map[*server.Resource]clientBuffer54+ frames uint64
52- dmabuf *dmabufGlobal55+ started time.Time
53- serial uint3256+ surfaces map[*server.Resource]*surface
57+ buffers map[*server.Resource]clientBuffer
58+ dmabuf *dmabufGlobal
59+ serial uint32
54 // press is the serial of the last button or key press, sent to60 // press is the serial of the last button or key press, sent to
55 // pressClient: popup grabs must come from it.61 // pressClient: popup grabs must come from it.
56 press uint3262 press uint32
@@ -136,7 +142,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
136 d.Close()142 d.Close()
137 return nil, err143 return nil, err
138 }144 }
139- s := &Server{display: d, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}145+ s := &Server{display: d, awaiting: map[string][]*wayland.Callback{}, frameDue: map[string]time.Time{}, env: linuxProcEnv{}, name: name, cleanup: cleanup, log: log, channels: ch, surfaces: make(map[*server.Resource]*surface), buffers: make(map[*server.Resource]clientBuffer), windows: make(map[ports.WindowID]*window), layers: make(map[ports.WindowID]*layerSurface), nextWindow: 1, eventReady: make(chan struct{}, 1), contents: make(map[ports.WindowID]ports.SurfaceContent), contentSeq: make(map[ports.WindowID]uint64), contentReady: make(chan struct{}, 1), cursorReady: make(chan struct{}, 1), dataSources: map[*server.Resource]*clipSource{}, primarySources: map[*server.Resource]*clipSource{}, controlSources: map[*server.Resource]*clipSource{}, contentNotify: make(chan struct{}), keymapFD: -1, keyboards: make(map[server.Client][]*wayland.Keyboard), pointers: make(map[server.Client][]*wayland.Pointer), regions: map[*server.Resource]*region{}, relatives: map[server.Client][]*relativepointer.ZwpRelativePointerV1{}, constraints: map[*surface]*constraint{}, positioners: map[*server.Resource]*positioner{}, repeatRate: opts.RepeatRate, repeatDelay: opts.RepeatDelay}
140 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}146 s.fractions = map[*surface]*fractionalscale.WpFractionalScaleV1{}
141 if opts.Keymap != "" {147 if opts.Keymap != "" {
142 fd, size, e := keymapFile(opts.Keymap)148 fd, size, e := keymapFile(opts.Keymap)
@@ -207,40 +213,7 @@ func (s *Server) Run(ctx context.Context) error {
207 }213 }
208 }214 }
209 }()215 }()
210- go func() {216+ go func() { defer wg.Done(); s.pace(ctx) }()
211- defer wg.Done()
212- ticker := time.NewTicker(16 * time.Millisecond)
213- defer ticker.Stop()
214- for {
215- select {
216- case <-ctx.Done():
217- return
218- case <-s.display.Stopped():
219- return
220- case <-ticker.C:
221- if ctx.Err() != nil {
222- return
223- }
224- if !s.display.Do(func() {
225- if ctx.Err() != nil {
226- return
227- }
228- callbacks := s.awaiting
229- s.awaiting = nil
230- for _, cb := range callbacks {
231- if !cb.Resource.Alive() {
232- continue
233- }
234- cb.SendDone(uint32(time.Since(s.started).Milliseconds()))
235- cb.Destroy()
236- s.frames++
237- }
238- }) {
239- return
240- }
241- }
242- }
243- }()
244 err := s.display.Run(ctx)217 err := s.display.Run(ctx)
245 wg.Wait()218 wg.Wait()
246 return err219 return err
M internal/adapters/wayland/surface.go +7 -1
@@ -197,7 +197,13 @@ func (s *surface) Commit(*wayland.Surface) {
197197 s.pending = nil
198198 s.attached = false
199199 }
200- s.server.awaiting = append(s.server.awaiting, s.callbacks...)
200+ if len(s.callbacks) > 0 {
201+ name := ""
202+ if o := s.server.outputOfSurface(s); o != nil {
203+ name = o.name()
204+ }
205+ s.server.awaiting[name] = append(s.server.awaiting[name], s.callbacks...)
206+ }
201207 s.callbacks = nil
202208 if s.role != nil {
203209 s.role(s.current != nil)
@@ -197,7 +197,13 @@ func (s *surface) Commit(*wayland.Surface) {
197 s.pending = nil197 s.pending = nil
198 s.attached = false198 s.attached = false
199 }199 }
200- s.server.awaiting = append(s.server.awaiting, s.callbacks...)200+ if len(s.callbacks) > 0 {
201+ name := ""
202+ if o := s.server.outputOfSurface(s); o != nil {
203+ name = o.name()
204+ }
205+ s.server.awaiting[name] = append(s.server.awaiting[name], s.callbacks...)
206+ }
201 s.callbacks = nil207 s.callbacks = nil
202 if s.role != nil {208 if s.role != nil {
203 s.role(s.current != nil)209 s.role(s.current != nil)
M internal/app/app.go +3 -2
@@ -83,6 +83,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
8383 renderScenes := make(chan []ports.Scene, 1)
8484 contents := make(chan ports.SurfaceContent, 64)
8585 cursorChanges := make(chan ports.CursorChange, 1)
86+ presented := make(chan ports.OutputPresented, 8)
8687 ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal}
8788 c, err := core.New(opts.Config, ch)
8889 if err != nil {
@@ -97,7 +98,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
9798 // Every output renders on the same GPU: its formats are the clients'.
9899 dmabuf := vulkan.Probe()
99100 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
100- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges}, logging.For(ctx, "wayland"))
101+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented}, logging.For(ctx, "wayland"))
101102 if err != nil {
102103 km.Close()
103104 return err
@@ -208,7 +209,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
208209 if hw != nil {
209210 done <- safe("output", func() error {
210211 want := func() drm.Want { return wantFromConfig(opts.Config.Outputs) }
211- return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, curs, newRenderer, logging.For(ctx, "drm"))
212+ return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))
212213 })
213214 return
214215 }
@@ -83,6 +83,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
83 renderScenes := make(chan []ports.Scene, 1)83 renderScenes := make(chan []ports.Scene, 1)
84 contents := make(chan ports.SurfaceContent, 64)84 contents := make(chan ports.SurfaceContent, 64)
85 cursorChanges := make(chan ports.CursorChange, 1)85 cursorChanges := make(chan ports.CursorChange, 1)
86+ presented := make(chan ports.OutputPresented, 8)
86 ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal}87 ch := core.Channels{Client: client, Input: input, Output: output, Config: configChanges, Commands: commands, Spawn: spawn, Scenes: scenes, Layouts: layouts, Constraints: constraints, State: states, ConfigErrors: configErrors, Terminal: !opts.NoTerminal}
87 c, err := core.New(opts.Config, ch)88 c, err := core.New(opts.Config, ch)
88 if err != nil {89 if err != nil {
@@ -97,7 +98,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
97 // Every output renders on the same GPU: its formats are the clients'.98 // Every output renders on the same GPU: its formats are the clients'.
98 dmabuf := vulkan.Probe()99 dmabuf := vulkan.Probe()
99 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")100 log.Info().Int("formats", len(dmabuf.Formats)).Msg("dmabuf")
100- server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges}, logging.For(ctx, "wayland"))101+ server, err := wayland.New(wayland.Options{RuntimeDir: runtimeDir, DMABuf: dmabuf, Keymap: keymap, RepeatRate: opts.Config.Keyboard.RepeatRate, RepeatDelay: opts.Config.Keyboard.RepeatDelay}, wayland.Channels{Events: client, Commands: commands, Contents: contents, Cursors: cursorChanges, Presented: presented}, logging.For(ctx, "wayland"))
101 if err != nil {102 if err != nil {
102 km.Close()103 km.Close()
103 return err104 return err
@@ -208,7 +209,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
208 if hw != nil {209 if hw != nil {
209 done <- safe("output", func() error {210 done <- safe("output", func() error {
210 want := func() drm.Want { return wantFromConfig(opts.Config.Outputs) }211 want := func() drm.Want { return wantFromConfig(opts.Config.Outputs) }
211- return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, curs, newRenderer, logging.For(ctx, "drm"))212+ return hw.runOutputs(ctx, want, output, renderScenes, contents, cursorChanges, presented, curs, newRenderer, logging.For(ctx, "drm"))
212 })213 })
213 return214 return
214 }215 }
M internal/app/drm.go +2 -2
@@ -108,7 +108,7 @@ func (b *drmBackend) close() {
108108 // one flip reader per card, and a udev watcher that rescans connectors on
109109 // hotplug. Core learns about outputs through events. It returns when ctx
110110 // ends, after every output is closed.
111-func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
111+func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
112112 ctx, cancel := context.WithCancel(ctx)
113113 defer cancel()
114114 var readers sync.WaitGroup
@@ -180,7 +180,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, event
180180 defer b.seat.Unsubscribe(active)
181181 return safe("output "+name, func() error {
182182 defer o.Close()
183- return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu)
183+ return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, presented)
184184 })
185185 })
186186 send(ports.OutputAdded{Info: o.Info()})
@@ -108,7 +108,7 @@ func (b *drmBackend) close() {
108 // one flip reader per card, and a udev watcher that rescans connectors on108 // one flip reader per card, and a udev watcher that rescans connectors on
109 // hotplug. Core learns about outputs through events. It returns when ctx109 // hotplug. Core learns about outputs through events. It returns when ctx
110 // ends, after every output is closed.110 // ends, after every output is closed.
111-func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {111+func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, events chan<- ports.OutputEvent, scenes <-chan []ports.Scene, contents <-chan ports.SurfaceContent, cursorChanges <-chan ports.CursorChange, presented chan<- ports.OutputPresented, curs *cursors, newRenderer func(w, h int) (ports.Renderer, error), log zerowrap.Logger) error {
112 ctx, cancel := context.WithCancel(ctx)112 ctx, cancel := context.WithCancel(ctx)
113 defer cancel()113 defer cancel()
114 var readers sync.WaitGroup114 var readers sync.WaitGroup
@@ -180,7 +180,7 @@ func (b *drmBackend) runOutputs(ctx context.Context, want func() drm.Want, event
180 defer b.seat.Unsubscribe(active)180 defer b.seat.Unsubscribe(active)
181 return safe("output "+name, func() error {181 return safe("output "+name, func() error {
182 defer o.Close()182 defer o.Close()
183- return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu)183+ return o.Run(octx, newRenderer, loadCursor, active, sc, cc, cu, presented)
184 })184 })
185 })185 })
186 send(ports.OutputAdded{Info: o.Info()})186 send(ports.OutputAdded{Info: o.Info()})
M internal/ports/messages.go +4 -0
@@ -278,6 +278,10 @@ type OutputAdded struct{ Info OutputInfo }
278278
279279 func (OutputAdded) outputEvent() {}
280280
281+// OutputPresented carries output → wayland that a frame reached the screen
282+// (a page flip completed): frame callbacks of the surfaces on it are due.
283+type OutputPresented struct{ Output string }
284+
281285 // OutputRemoved carries output → core an unplugged display.
282286 type OutputRemoved struct{ Name string }
283287
@@ -278,6 +278,10 @@ type OutputAdded struct{ Info OutputInfo }
278 278
279 func (OutputAdded) outputEvent() {}279 func (OutputAdded) outputEvent() {}
280 280
281+// OutputPresented carries output → wayland that a frame reached the screen
282+// (a page flip completed): frame callbacks of the surfaces on it are due.
283+type OutputPresented struct{ Output string }
284+
281 // OutputRemoved carries output → core an unplugged display.285 // OutputRemoved carries output → core an unplugged display.
282 type OutputRemoved struct{ Name string }286 type OutputRemoved struct{ Name string }
283 287

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