Internal
Public Access
Add VNC remote cursor shape sync
Implements RFB's Cursor pseudo-encoding (RFC 6143 SS7.8.2, type -239): a FramebufferUpdate rectangle carrying a cursor shape instead of screen content (x/y are the hotspot, not position; width/height are the cursor image size), decoded into an ARGB32 QImage using the rectangle's RGB pixel data plus its opacity bitmask, then never painted into the framebuffer. A 0x0 rectangle means "hide the cursor" per spec. VncDisplayWidget gains RdpDisplayWidget's setCursorImage/Hidden/ Default() + applyCursor() shape, reusing its own renderRect()/ effectiveRemoteSize() so cursor scaling works correctly in both the scale-to-fit and actual-size display modes with no special-casing. VNC never emits cursorReset() (RFB's Cursor pseudo-encoding has no "reset to system default" signal, unlike RDP's SetDefault callback) -- setCursorDefault() exists for symmetry but is unused today. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,8 +32,13 @@ constexpr quint8 kServerMsgServerCutText = 3;
|
||||
// of truth for what we tell the server we can decode via SetEncodings.
|
||||
constexpr qint32 kEncRaw = 0;
|
||||
constexpr qint32 kEncCopyRect = 1;
|
||||
// RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content
|
||||
// encoding -- a rectangle with this type carries a cursor shape update
|
||||
// instead (hotspot in x/y, image dims in width/height), never painted into
|
||||
// the framebuffer.
|
||||
constexpr qint32 kEncCursor = -239;
|
||||
|
||||
constexpr std::array<qint32, 2> kAnnouncedEncodings = { kEncRaw, kEncCopyRect };
|
||||
constexpr std::array<qint32, 3> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncCursor };
|
||||
|
||||
quint16 readU16BE(const QByteArray& buf, int offset)
|
||||
{
|
||||
@@ -320,6 +325,7 @@ void VncSessionBackend::onSocketDisconnected()
|
||||
&& m_rfbState != RfbState::WaitingRectangleHeader
|
||||
&& m_rfbState != RfbState::WaitingRawPixelData
|
||||
&& m_rfbState != RfbState::WaitingCopyRectSource
|
||||
&& m_rfbState != RfbState::WaitingCursorPixelData
|
||||
&& m_rfbState != RfbState::WaitingSetColourMapHeader
|
||||
&& m_rfbState != RfbState::WaitingSetColourMapData
|
||||
&& m_rfbState != RfbState::WaitingServerCutTextHeader
|
||||
@@ -810,6 +816,9 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
case kEncCopyRect:
|
||||
m_rfbState = RfbState::WaitingCopyRectSource;
|
||||
break;
|
||||
case kEncCursor:
|
||||
m_rfbState = RfbState::WaitingCursorPixelData;
|
||||
break;
|
||||
default: {
|
||||
// SetEncodings (see kAnnouncedEncodings) is entirely
|
||||
// client-controlled, so a spec-compliant server will never
|
||||
@@ -878,6 +887,60 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingCursorPixelData: {
|
||||
// Cursor pseudo-encoding (RFC 6143 SS7.8.2): x/y in the already-
|
||||
// parsed rectangle header are the hotspot, not screen position;
|
||||
// width/height are the cursor image's own dimensions. Never
|
||||
// painted into m_framebuffer. Payload is width*height pixels in
|
||||
// our negotiated 32bpp format, followed by a row-padded,
|
||||
// MSB-first-per-byte opacity bitmask.
|
||||
const int width = m_currentRectangle.width;
|
||||
const int height = m_currentRectangle.height;
|
||||
const qint64 maskRowBytes = (static_cast<qint64>(width) + 7) / 8;
|
||||
const qint64 pixelBytes = static_cast<qint64>(width) * height * 4;
|
||||
const qint64 maskBytes = maskRowBytes * height;
|
||||
const qint64 totalBytes = pixelBytes + maskBytes;
|
||||
if (totalBytes < 0 || totalBytes > std::numeric_limits<int>::max()) {
|
||||
failConnection(QStringLiteral("The VNC server sent an implausibly large cursor image."),
|
||||
QStringLiteral("Cursor rectangle %1x%2").arg(width).arg(height));
|
||||
return;
|
||||
}
|
||||
if (!haveBytes(static_cast<int>(totalBytes))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A 0x0 cursor rectangle is the spec's way of saying "hide the
|
||||
// cursor"; treat any other degenerate (zero-area) size the same
|
||||
// way rather than trying to build an empty QImage.
|
||||
if (width <= 0 || height <= 0) {
|
||||
emit cursorHidden();
|
||||
} else {
|
||||
QImage cursorImage(width, height, QImage::Format_ARGB32);
|
||||
const auto* pixelData = reinterpret_cast<const uchar*>(m_recvBuffer.constData());
|
||||
const uchar* maskData = pixelData + pixelBytes;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const uchar* px = pixelData + ((static_cast<qint64>(y) * width + x) * 4);
|
||||
// Matches our negotiated SetPixelFormat: little-
|
||||
// endian 32bpp, R at shift 16 / G at 8 / B at 0 --
|
||||
// byte order B,G,R,pad.
|
||||
const uchar b = px[0];
|
||||
const uchar g = px[1];
|
||||
const uchar r = px[2];
|
||||
const uchar maskByte = maskData[y * maskRowBytes + (x / 8)];
|
||||
const bool opaque = (maskByte & (0x80 >> (x % 8))) != 0;
|
||||
cursorImage.setPixel(x, y, qRgba(r, g, b, opaque ? 255 : 0));
|
||||
}
|
||||
}
|
||||
emit cursorImageChanged(cursorImage,
|
||||
QPoint(m_currentRectangle.x, m_currentRectangle.y));
|
||||
}
|
||||
|
||||
m_recvBuffer.remove(0, static_cast<int>(totalBytes));
|
||||
onRectangleFinished();
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingSetColourMapHeader: {
|
||||
if (!haveBytes(5)) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user