Retry a transient cursor-source failure and stop reporting stale framebuffer geometry

This commit is contained in:
Viktor Liu
2026-09-22 19:56:19 +02:00
parent 67c786b4f4
commit d1d8c19fc7
3 changed files with 39 additions and 6 deletions
+16 -4
View File
@@ -53,22 +53,33 @@ func (p *FBPoller) ClientDisconnect() {
if p.clients <= 0 && p.capturer != nil { if p.clients <= 0 && p.capturer != nil {
p.capturer.Close() p.capturer.Close()
p.capturer = nil p.capturer = nil
// Drop the geometry with the device. The next client re-reads it, and
// the framebuffer may be gone or resized by then.
p.w, p.h = 0, 0
} }
} }
// Width returns the framebuffer width, doing lazy init if needed. // Width returns the framebuffer width, doing lazy init if needed. Zero when
// the device cannot be opened: the geometry cached from an earlier session is
// no evidence the framebuffer is still there, and reporting it admits a VNC
// session whose every frame then fails.
func (p *FBPoller) Width() int { func (p *FBPoller) Width() int {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
_ = p.ensureCapturerLocked() if err := p.ensureCapturerLocked(); err != nil {
return 0
}
return p.w return p.w
} }
// Height returns the framebuffer height, doing lazy init if needed. // Height returns the framebuffer height, doing lazy init if needed. Zero when
// the device cannot be opened, for the reason given on Width.
func (p *FBPoller) Height() int { func (p *FBPoller) Height() int {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
_ = p.ensureCapturerLocked() if err := p.ensureCapturerLocked(); err != nil {
return 0
}
return p.h return p.h
} }
@@ -103,6 +114,7 @@ func (p *FBPoller) Close() {
p.capturer.Close() p.capturer.Close()
p.capturer = nil p.capturer = nil
} }
p.w, p.h = 0, 0
} }
func (p *FBPoller) ensureCapturerLocked() error { func (p *FBPoller) ensureCapturerLocked() error {
+7
View File
@@ -114,6 +114,12 @@ type session struct {
// source so the encoder stops polling for the rest of the session. // source so the encoder stops polling for the rest of the session.
// Reset on SetEncodings so a reconnect can retry. // Reset on SetEncodings so a reconnect can retry.
cursorSourceFailed bool cursorSourceFailed bool
// cursorSourceFailures counts consecutive errors from the cursor source.
// A single error does not latch: the Windows sampler fails across a
// desktop switch and recovers on the next capture, and latching there
// costs the client its cursor for the rest of the session. Reset by the
// first success.
cursorSourceFailures int
// showRemoteCursor switches the encoder to compositing the server // showRemoteCursor switches the encoder to compositing the server
// cursor sprite into the captured framebuffer at the remote position // cursor sprite into the captured framebuffer at the remote position
// instead of emitting the Cursor pseudo-encoding. Toggled by the // instead of emitting the Cursor pseudo-encoding. Toggled by the
@@ -489,6 +495,7 @@ func (s *session) resetEncodingCaps() {
s.clientSupportsCursor = false s.clientSupportsCursor = false
s.clientSupportsExtMouseButtons = false s.clientSupportsExtMouseButtons = false
s.cursorSourceFailed = false s.cursorSourceFailed = false
s.cursorSourceFailures = 0
s.clientJPEGQuality = -1 s.clientJPEGQuality = -1
s.clientZlibLevel = -1 s.clientZlibLevel = -1
} }
+16 -2
View File
@@ -8,6 +8,13 @@ import (
"image" "image"
) )
// cursorSourceFailureLimit is how many consecutive Cursor() errors are taken as
// the source being genuinely unavailable rather than momentarily unreadable.
// Transient failures are normal: the Windows sampler cannot read the cursor
// across a desktop switch or while the secure desktop is up, and recovers by
// the next capture.
const cursorSourceFailureLimit = 10
// pendingCursorRect returns the Cursor pseudo-rect for the current sprite // pendingCursorRect returns the Cursor pseudo-rect for the current sprite
// when the client negotiated the encoding and the platform exposes a // when the client negotiated the encoding and the platform exposes a
// cursor source whose serial has changed since the last emission. A nil // cursor source whose serial has changed since the last emission. A nil
@@ -46,11 +53,18 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
img, hotX, hotY, serial, err := src.Cursor() img, hotX, hotY, serial, err := src.Cursor()
if err != nil { if err != nil {
s.encMu.Lock() s.encMu.Lock()
s.cursorSourceFailed = true s.cursorSourceFailures++
failures := s.cursorSourceFailures
if failures >= cursorSourceFailureLimit {
s.cursorSourceFailed = true
}
s.encMu.Unlock() s.encMu.Unlock()
s.log.Debugf("cursor source unavailable: %v", err) s.log.Debugf("cursor source unavailable (%d consecutive): %v", failures, err)
return nil return nil
} }
s.encMu.Lock()
s.cursorSourceFailures = 0
s.encMu.Unlock()
if img == nil { if img == nil {
s.logCursorSkip("no-sprite", "no cursor rect: capturer returned no sprite") s.logCursorSkip("no-sprite", "no cursor rect: capturer returned no sprite")
return nil return nil