Attach to the X server on the active VT and keep a retryable DXGI frame from tearing down the capturer

Claude-Session: https://claude.ai/code/session_01QKDYfH4WKLbpNQHccpVo3P
This commit is contained in:
Viktor Liu
2026-08-29 16:59:05 +02:00
parent 15d6e3bea9
commit fecd7cfff5
17 changed files with 476 additions and 85 deletions
+30 -15
View File
@@ -4,6 +4,7 @@ package server
import (
"encoding/binary"
"fmt"
"image"
)
@@ -29,17 +30,13 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
// cannot produce one, or the sprite failed to encode. Say which, once per
// session, so the next report of a missing cursor names its own cause.
if !supported || failed || composite {
s.cursorSkipOnce.Do(func() {
s.log.Debugf("no cursor rect: client_requested=%v source_failed=%v compositing=%v",
supported, failed, composite)
})
s.logCursorSkip("no cursor rect: client_requested=%v source_failed=%v compositing=%v",
supported, failed, composite)
return nil
}
src, ok := s.capturer.(cursorSource)
if !ok {
s.cursorSkipOnce.Do(func() {
s.log.Debugf("no cursor rect: capturer %T reports no cursor source", s.capturer)
})
s.logCursorSkip("no cursor rect: capturer %T reports no cursor source", s.capturer)
return nil
}
img, hotX, hotY, serial, err := src.Cursor()
@@ -51,9 +48,7 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
return nil
}
if img == nil {
s.cursorSkipOnce.Do(func() {
s.log.Debug("no cursor rect: capturer returned no sprite")
})
s.logCursorSkip("no cursor rect: capturer returned no sprite")
return nil
}
if serial == lastSerial {
@@ -61,11 +56,9 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
}
buf := encodeCursorPseudoRect(img, hotX, hotY, pf)
if buf == nil {
s.cursorSkipOnce.Do(func() {
b := img.Bounds()
s.log.Debugf("no cursor rect: sprite %dx%d stride=%d pix=%d could not be encoded",
b.Dx(), b.Dy(), img.Stride, len(img.Pix))
})
b := img.Bounds()
s.logCursorSkip("no cursor rect: sprite %dx%d stride=%d pix=%d could not be encoded",
b.Dx(), b.Dy(), img.Stride, len(img.Pix))
return nil
}
// Re-check under the write lock so a cursor another goroutine already
@@ -89,6 +82,28 @@ func (s *session) pendingCursorRect(pf clientPixelFormat) []byte {
return buf
}
// logCursorSkip reports why this update carries no cursor rect, once per
// distinct line. pendingCursorRect runs per framebuffer update, so an
// unthrottled log would flood; throttling once per session instead would let
// whichever reason came first hide every later one, and the reasons do change
// as the client negotiates encodings and the cursor source starts failing.
func (s *session) logCursorSkip(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
s.cursorSkipMu.Lock()
if s.cursorSkipSeen == nil {
s.cursorSkipSeen = make(map[string]struct{})
}
_, seen := s.cursorSkipSeen[msg]
s.cursorSkipSeen[msg] = struct{}{}
s.cursorSkipMu.Unlock()
if seen {
return
}
s.log.Debug(msg)
}
// maxCursorDim caps the cursor sprite size we'll encode. Real platform
// cursors are tiny (<=256×256 on every supported OS); a value past this
// almost certainly indicates a corrupted platform-API response, and