diff --git a/client/vnc/server/cursor_x11.go b/client/vnc/server/cursor_x11.go index 2ac8d2798..a00ddf55c 100644 --- a/client/vnc/server/cursor_x11.go +++ b/client/vnc/server/cursor_x11.go @@ -17,15 +17,20 @@ import ( type xfixesCursor struct { mu sync.Mutex conn *xgb.Conn - // runtimeErr latches the first GetCursorImage failure so subsequent - // calls return quickly without another X round-trip. Some virtual - // displays advertise XFixes but reject GetCursorImage (Xvfb). - runtimeErr error // lastPosX/lastPosY hold the cursor screen position observed on the // most recent successful GetCursorImage. cursorPositionSource readers // share this value so we do not pay a second X round-trip per frame. lastPosX, lastPosY int hasPos bool + // lastImg, lastHotX, lastHotY, lastSerial cache the most recent good + // GetCursorImage result so transient failures (cursor hidden, server + // briefly unresponsive) reuse the previous sprite instead of going + // dark. Without this the encoder's compositing path drops to no-op as + // soon as the cursor becomes momentarily unavailable. + lastImg *image.RGBA + lastHotX int + lastHotY int + lastSerial uint64 } // newXFixesCursor initialises the XFixes extension on conn. Returns an @@ -42,24 +47,31 @@ func newXFixesCursor(conn *xgb.Conn) (*xfixesCursor, error) { } // Cursor returns the current cursor sprite as RGBA along with its hotspot -// and serial. Callers should treat an unchanged serial as "no update". +// and serial. Callers should treat an unchanged serial as "no update". On +// a transient GetCursorImage failure the last cached sprite is returned +// so compositing keeps painting the cursor instead of disappearing. func (c *xfixesCursor) Cursor() (*image.RGBA, int, int, uint64, error) { c.mu.Lock() defer c.mu.Unlock() - if c.runtimeErr != nil { - return nil, 0, 0, 0, c.runtimeErr - } reply, err := xfixes.GetCursorImage(c.conn).Reply() if err != nil { - c.runtimeErr = fmt.Errorf("xfixes GetCursorImage: %w", err) - return nil, 0, 0, 0, c.runtimeErr + if c.lastImg != nil { + return c.lastImg, c.lastHotX, c.lastHotY, c.lastSerial, nil + } + return nil, 0, 0, 0, fmt.Errorf("xfixes GetCursorImage: %w", err) } c.lastPosX, c.lastPosY, c.hasPos = int(reply.X), int(reply.Y), true w, h := int(reply.Width), int(reply.Height) if w <= 0 || h <= 0 { + if c.lastImg != nil { + return c.lastImg, c.lastHotX, c.lastHotY, c.lastSerial, nil + } return nil, 0, 0, 0, fmt.Errorf("cursor has zero extent") } if len(reply.CursorImage) < w*h { + if c.lastImg != nil { + return c.lastImg, c.lastHotX, c.lastHotY, c.lastSerial, nil + } return nil, 0, 0, 0, fmt.Errorf("cursor pixel buffer truncated: %d < %d", len(reply.CursorImage), w*h) } img := image.NewRGBA(image.Rect(0, 0, w, h)) @@ -72,7 +84,11 @@ func (c *xfixesCursor) Cursor() (*image.RGBA, int, int, uint64, error) { img.Pix[o+2] = byte(p) img.Pix[o+3] = byte(p >> 24) } - return img, int(reply.Xhot), int(reply.Yhot), uint64(reply.CursorSerial), nil + c.lastImg = img + c.lastHotX = int(reply.Xhot) + c.lastHotY = int(reply.Yhot) + c.lastSerial = uint64(reply.CursorSerial) + return img, c.lastHotX, c.lastHotY, c.lastSerial, nil } // Cursor on X11Capturer satisfies cursorSource. The XFixes binding is diff --git a/client/vnc/server/server.go b/client/vnc/server/server.go index aff21ec9f..3be5044cd 100644 --- a/client/vnc/server/server.go +++ b/client/vnc/server/server.go @@ -604,10 +604,6 @@ func (s *Server) handleConnection(conn net.Conn) { serverW: capturer.Width(), serverH: capturer.Height(), log: connLog, - // Virtual sessions run on Xvfb which has no usable cursor source, - // so we skip the Cursor pseudo-encoding and let the client's - // local fallback show instead. - disableCursor: header.mode == ModeSession, } sess.serve() } diff --git a/client/vnc/server/session.go b/client/vnc/server/session.go index bd355a780..495f32c64 100644 --- a/client/vnc/server/session.go +++ b/client/vnc/server/session.go @@ -99,10 +99,6 @@ type session struct { // source so the encoder stops polling for the rest of the session. // Reset on SetEncodings so a reconnect can retry. cursorSourceFailed bool - // disableCursor suppresses the Cursor pseudo-encoding regardless of - // what the client advertises. Set for virtual sessions where no - // usable cursor source exists. Constant for the session lifetime. - disableCursor bool // showRemoteCursor switches the encoder to compositing the server // cursor sprite into the captured framebuffer at the remote position // instead of emitting the Cursor pseudo-encoding. Toggled by the @@ -462,9 +458,6 @@ func (s *session) applyEncoding(enc int32) string { s.clientSupportsExtClipboard = true return "ext-clipboard" case pseudoEncCursor: - if s.disableCursor { - return "" - } s.clientSupportsCursor = true return "cursor" case pseudoEncExtendedMouseButtons: