detached 5f079f4

fix(wayland): bind posted callbacks before the sync waiter and log pace timer failures

commit
5f079f4ca2c8fef180a13f7b6b193789186da803
author
brice · · 2026-10-13 17:58 UTC
committer
brice · · 2026-10-14 04:34 UTC
parent
50ff603
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +24 -20
M internal/adapters/wayland/frames.go +19 -3
@@ -1,6 +1,7 @@
11 package wayland
22
33 import (
4+ "errors"
45 "slices"
56 "time"
67
@@ -47,20 +48,35 @@ func (s *Server) armPace(d time.Duration) {
4748 d = time.Nanosecond
4849 }
4950 spec := unix.ItimerSpec{Value: unix.NsecToTimespec(int64(d))}
50- _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil)
51+ s.warnPace("arm", unix.TimerfdSettime(s.paceFD, 0, &spec, nil))
5152 }
5253
5354 // stopPace disarms the pace timerfd. Display goroutine only.
5455 func (s *Server) stopPace() {
5556 var spec unix.ItimerSpec
56- _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil)
57+ s.warnPace("stop", unix.TimerfdSettime(s.paceFD, 0, &spec, nil))
58+}
59+
60+// warnPace logs the first failure of a pace timerfd operation (later ones
61+// would repeat at frame rate). A failed arm leaves frame callbacks waiting
62+// for the next commit or flip. Display goroutine only.
63+func (s *Server) warnPace(op string, err error) {
64+ if err == nil || s.paceWarned {
65+ return
66+ }
67+ s.paceWarned = true
68+ s.log.Warn().Err(err).Str("op", op).Msg("pace timer")
5769 }
5870
5971 // onPaceTimer is the display loop's callback for the pace timerfd: the
6072 // deadline armed by the last tick passed.
6173 func (s *Server) onPaceTimer() {
6274 var buf [8]byte
63- _, _ = unix.Read(s.paceFD, buf[:]) // level-triggered: consume the expiry
75+ // Level-triggered: consume the expiry. EAGAIN: the timer was disarmed
76+ // after the loop saw it readable.
77+ if _, err := unix.Read(s.paceFD, buf[:]); err != nil && !errors.Is(err, unix.EAGAIN) {
78+ s.warnPace("read", err)
79+ }
6480 s.paceTick(nil, nil)
6581 }
6682
@@ -1,6 +1,7 @@
1 package wayland1 package wayland
2 2
3 import (3 import (
4+ "errors"
4 "slices"5 "slices"
5 "time"6 "time"
6 7
@@ -47,20 +48,35 @@ func (s *Server) armPace(d time.Duration) {
47 d = time.Nanosecond48 d = time.Nanosecond
48 }49 }
49 spec := unix.ItimerSpec{Value: unix.NsecToTimespec(int64(d))}50 spec := unix.ItimerSpec{Value: unix.NsecToTimespec(int64(d))}
50- _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil)51+ s.warnPace("arm", unix.TimerfdSettime(s.paceFD, 0, &spec, nil))
51 }52 }
52 53
53 // stopPace disarms the pace timerfd. Display goroutine only.54 // stopPace disarms the pace timerfd. Display goroutine only.
54 func (s *Server) stopPace() {55 func (s *Server) stopPace() {
55 var spec unix.ItimerSpec56 var spec unix.ItimerSpec
56- _ = unix.TimerfdSettime(s.paceFD, 0, &spec, nil)57+ s.warnPace("stop", unix.TimerfdSettime(s.paceFD, 0, &spec, nil))
58+}
59+
60+// warnPace logs the first failure of a pace timerfd operation (later ones
61+// would repeat at frame rate). A failed arm leaves frame callbacks waiting
62+// for the next commit or flip. Display goroutine only.
63+func (s *Server) warnPace(op string, err error) {
64+ if err == nil || s.paceWarned {
65+ return
66+ }
67+ s.paceWarned = true
68+ s.log.Warn().Err(err).Str("op", op).Msg("pace timer")
57 }69 }
58 70
59 // onPaceTimer is the display loop's callback for the pace timerfd: the71 // onPaceTimer is the display loop's callback for the pace timerfd: the
60 // deadline armed by the last tick passed.72 // deadline armed by the last tick passed.
61 func (s *Server) onPaceTimer() {73 func (s *Server) onPaceTimer() {
62 var buf [8]byte74 var buf [8]byte
63- _, _ = unix.Read(s.paceFD, buf[:]) // level-triggered: consume the expiry75+ // Level-triggered: consume the expiry. EAGAIN: the timer was disarmed
76+ // after the loop saw it readable.
77+ if _, err := unix.Read(s.paceFD, buf[:]); err != nil && !errors.Is(err, unix.EAGAIN) {
78+ s.warnPace("read", err)
79+ }
64 s.paceTick(nil, nil)80 s.paceTick(nil, nil)
65 }81 }
66 82
M internal/adapters/wayland/server.go +5 -2
@@ -156,6 +156,7 @@ type Server struct {
156156 // goroutine only). paceSignal coalesces posted ticks (paceSignalFn).
157157 paceFD int
158158 paceReady bool
159+ paceWarned bool // a pace timer failure was logged (once)
159160 paceSignal atomic.Bool
160161 paceSignalFn func()
161162 paceDepth int
@@ -348,7 +349,11 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
348349 positioners: map[*server.Resource]*positioner{},
349350 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},
350351 }
352+ // Bound once, before anything can post them (the sync waiter calls
353+ // signalPacer from its own goroutine as soon as it exists).
354+ s.paceSignalFn = s.paceSignalled
351355 s.flushLinksFn = s.flushLinks
356+ s.sink = newPresentSink(s)
352357 s.leaseDevices = map[string]*leaseDevice{}
353358 s.pendingLeases = map[uint64]*leaseObject{}
354359 s.pendingLeaseCards = map[uint64]string{}
@@ -421,8 +426,6 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
421426 s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
422427 }
423428 s.syncWait.log = log
424- s.sink = newPresentSink(s)
425- s.paceSignalFn = s.paceSignalled
426429 s.cleanup = func() {
427430 if s.paceReady {
428431 unix.Close(s.paceFD)
@@ -156,6 +156,7 @@ type Server struct {
156 // goroutine only). paceSignal coalesces posted ticks (paceSignalFn).156 // goroutine only). paceSignal coalesces posted ticks (paceSignalFn).
157 paceFD int157 paceFD int
158 paceReady bool158 paceReady bool
159+ paceWarned bool // a pace timer failure was logged (once)
159 paceSignal atomic.Bool160 paceSignal atomic.Bool
160 paceSignalFn func()161 paceSignalFn func()
161 paceDepth int162 paceDepth int
@@ -348,7 +349,11 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
348 positioners: map[*server.Resource]*positioner{},349 positioners: map[*server.Resource]*positioner{},
349 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},350 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},
350 }351 }
352+ // Bound once, before anything can post them (the sync waiter calls
353+ // signalPacer from its own goroutine as soon as it exists).
354+ s.paceSignalFn = s.paceSignalled
351 s.flushLinksFn = s.flushLinks355 s.flushLinksFn = s.flushLinks
356+ s.sink = newPresentSink(s)
352 s.leaseDevices = map[string]*leaseDevice{}357 s.leaseDevices = map[string]*leaseDevice{}
353 s.pendingLeases = map[uint64]*leaseObject{}358 s.pendingLeases = map[uint64]*leaseObject{}
354 s.pendingLeaseCards = map[uint64]string{}359 s.pendingLeaseCards = map[uint64]string{}
@@ -421,8 +426,6 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
421 s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap426 s.seat.keymapFD, s.seat.keymapSize, s.seat.keymapText = fd, size, opts.Keymap
422 }427 }
423 s.syncWait.log = log428 s.syncWait.log = log
424- s.sink = newPresentSink(s)
425- s.paceSignalFn = s.paceSignalled
426 s.cleanup = func() {429 s.cleanup = func() {
427 if s.paceReady {430 if s.paceReady {
428 unix.Close(s.paceFD)431 unix.Close(s.paceFD)
D internal/app/contentattach.go +0 -15
@@ -1,15 +0,0 @@
1-package app
2-
3-import (
4- "github.com/bnema/neferwl/internal/adapters/wayland"
5- "github.com/bnema/neferwl/internal/ports"
6-)
7-
8-// waylandContent is the contentAttacher of a wayland server.
9-type waylandContent struct{ server *wayland.Server }
10-
11-func (w waylandContent) Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) {
12- return w.server.AttachOutput(name)
13-}
14-
15-func (w waylandContent) Detach(name string) { w.server.DetachOutput(name) }
@@ -1,15 +0,0 @@
1-package app
2-
3-import (
4- "github.com/bnema/neferwl/internal/adapters/wayland"
5- "github.com/bnema/neferwl/internal/ports"
6-)
7-
8-// waylandContent is the contentAttacher of a wayland server.
9-type waylandContent struct{ server *wayland.Server }
10-
11-func (w waylandContent) Attach(name string) (<-chan *ports.ContentBatch, ports.ContentReturn) {
12- return w.server.AttachOutput(name)
13-}
14-
15-func (w waylandContent) Detach(name string) { w.server.DetachOutput(name) }
R internal/app/contentattach_test.go → internal/app/contentattacher_test.go +0 -0 renamed, 100% similar

renamed, 100% similar

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