Add VNC Hextile decoding

Implements RFC 6143 SS7.7.4: rectangles announced as Hextile (type 5)
tile the update into 16x16 blocks, each either raw pixels or a
background fill plus an optional list of foreground/individually-
colored subrects, with background/foreground persisting across tiles
within one rectangle when not re-specified.

The pure byte-decode logic (tile metadata, subrect list) lives in new
src/vnc_pixel_codecs.h/.cpp, kept separate from
VncSessionBackend's wire-sequencing state machine so it's unit-testable
without a socket -- the pattern the plan calls for continuing into the
ZRLE/Tight work still ahead. Adds 5 fake-server tests covering a raw
tile, a background-only solid fill, uncoloured and individually-colored
subrects, and a 4-tile rectangle proving background persistence and
correct tile-cursor wraparound.

Verified against the live TightVNC test server (connect, frame,
cursor, clipboard all still work); that particular server always
chose Raw for the actual framebuffer content during this session, so
Hextile's real-world path isn't independently confirmed live -- the
unit tests are the primary correctness evidence for this phase.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 20:49:08 -06:00
co-authored by Claude Sonnet 5
parent 9dd1af21d6
commit e49fa0cf26
7 changed files with 598 additions and 6 deletions
+2
View File
@@ -128,6 +128,8 @@ set(ORBITHUB_SOURCES
src/ssh_session_backend.h src/ssh_session_backend.h
src/vnc_session_backend.cpp src/vnc_session_backend.cpp
src/vnc_session_backend.h src/vnc_session_backend.h
src/vnc_pixel_codecs.cpp
src/vnc_pixel_codecs.h
src/unsupported_session_backend.cpp src/unsupported_session_backend.cpp
src/unsupported_session_backend.h src/unsupported_session_backend.h
) )
+80
View File
@@ -0,0 +1,80 @@
#include "vnc_pixel_codecs.h"
namespace VncPixelCodecs {
QRgb rgbFromPixelBytes(const uchar* bytes)
{
// Byte order B,G,R,pad -- matches VncSessionBackend's negotiated
// SetPixelFormat (32bpp little-endian, R at shift 16 / G at 8 / B at 0).
return qRgb(bytes[2], bytes[1], bytes[0]);
}
int hextileFixedMetaByteCount(quint8 subencoding)
{
int count = 0;
if ((subencoding & HextileFlags::kBackgroundSpecified) != 0) {
count += 4;
}
if ((subencoding & HextileFlags::kForegroundSpecified) != 0) {
count += 4;
}
if ((subencoding & HextileFlags::kAnySubrects) != 0) {
count += 1;
}
return count;
}
int decodeHextileFixedMeta(quint8 subencoding, const QByteArray& data, QRgb* background,
QRgb* foreground)
{
int offset = 0;
const auto* bytes = reinterpret_cast<const uchar*>(data.constData());
if ((subencoding & HextileFlags::kBackgroundSpecified) != 0) {
*background = rgbFromPixelBytes(bytes + offset);
offset += 4;
}
if ((subencoding & HextileFlags::kForegroundSpecified) != 0) {
*foreground = rgbFromPixelBytes(bytes + offset);
offset += 4;
}
if ((subencoding & HextileFlags::kAnySubrects) != 0) {
return static_cast<int>(static_cast<quint8>(data.at(offset)));
}
return 0;
}
int hextileSubrectByteCount(bool coloured, int subrectCount)
{
return subrectCount * (coloured ? 6 : 2);
}
QVector<HextileSubrect> decodeHextileSubrects(bool coloured, int subrectCount,
const QByteArray& data, QRgb foreground)
{
QVector<HextileSubrect> subrects;
subrects.reserve(subrectCount);
const auto* bytes = reinterpret_cast<const uchar*>(data.constData());
const int stride = coloured ? 6 : 2;
for (int i = 0; i < subrectCount; ++i) {
const uchar* entry = bytes + (i * stride);
QRgb color = foreground;
int fieldOffset = 0;
if (coloured) {
color = rgbFromPixelBytes(entry);
fieldOffset = 4;
}
const uchar xy = entry[fieldOffset];
const uchar wh = entry[fieldOffset + 1];
// High nibble = x (or width-1), low nibble = y (or height-1).
const int x = (xy >> 4) & 0x0F;
const int y = xy & 0x0F;
const int width = ((wh >> 4) & 0x0F) + 1;
const int height = (wh & 0x0F) + 1;
subrects.append(HextileSubrect{QRect(x, y, width, height), color});
}
return subrects;
}
}
+65
View File
@@ -0,0 +1,65 @@
#ifndef ORBITHUB_VNC_PIXEL_CODECS_H
#define ORBITHUB_VNC_PIXEL_CODECS_H
#include <QByteArray>
#include <QRect>
#include <QRgb>
#include <QVector>
// Pure, state-free decode helpers for VNC/RFB pixel encodings, kept out of
// VncSessionBackend so the wire-sequencing (when to read what off the
// socket) and wire-decoding (how to interpret already-buffered bytes) stay
// separate and the latter is unit-testable without a live connection.
namespace VncPixelCodecs {
// Converts one pixel's worth of raw bytes as sent under
// VncSessionBackend's negotiated SetPixelFormat (32bpp, little-endian,
// byte order B,G,R,pad) into a QRgb.
QRgb rgbFromPixelBytes(const uchar* bytes);
// A single filled rectangle, in tile-local coordinates (0,0 = the tile's
// own top-left corner, not the enclosing FramebufferUpdate rectangle's).
struct HextileSubrect {
QRect rect;
QRgb color = 0;
};
// RFC 6143 SS7.7.4 Hextile tile subencoding byte flags.
namespace HextileFlags {
constexpr quint8 kRaw = 0x01;
constexpr quint8 kBackgroundSpecified = 0x02;
constexpr quint8 kForegroundSpecified = 0x04;
constexpr quint8 kAnySubrects = 0x08;
constexpr quint8 kSubrectsColoured = 0x10;
}
// Byte length of a Hextile tile's "fixed" metadata -- the optional
// background/foreground color updates plus the optional subrect count --
// derivable from the subencoding byte alone, before any of those bytes are
// available. Only meaningful when HextileFlags::kRaw is *not* set (a Raw
// tile has no metadata at all, just tileWidth*tileHeight raw pixels).
int hextileFixedMetaByteCount(quint8 subencoding);
// Parses the `hextileFixedMetaByteCount(subencoding)` bytes described
// above. Updates *background/*foreground in place only when the
// corresponding flag is set in `subencoding` -- callers should persist
// their previous values across tiles in the same rectangle and pass them
// in here unchanged when a color isn't re-specified, since RFC 6143 has
// each tile inherit the last-specified colors. Returns the subrect count
// (0 if HextileFlags::kAnySubrects isn't set).
int decodeHextileFixedMeta(quint8 subencoding, const QByteArray& data, QRgb* background,
QRgb* foreground);
// Byte length of `subrectCount` subrects' worth of data, given whether
// they're individually colored (HextileFlags::kSubrectsColoured).
int hextileSubrectByteCount(bool coloured, int subrectCount);
// Parses `subrectCount` subrects (xy + wh bytes, plus a per-subrect color
// when `coloured`) out of `data`, substituting `foreground` in for any
// that aren't individually colored.
QVector<HextileSubrect> decodeHextileSubrects(bool coloured, int subrectCount,
const QByteArray& data, QRgb foreground);
}
#endif
+144 -2
View File
@@ -1,5 +1,7 @@
#include "vnc_session_backend.h" #include "vnc_session_backend.h"
#include "vnc_pixel_codecs.h"
#include <QPainter> #include <QPainter>
#include <QStringList> #include <QStringList>
#include <QTcpSocket> #include <QTcpSocket>
@@ -32,13 +34,15 @@ constexpr quint8 kServerMsgServerCutText = 3;
// of truth for what we tell the server we can decode via SetEncodings. // of truth for what we tell the server we can decode via SetEncodings.
constexpr qint32 kEncRaw = 0; constexpr qint32 kEncRaw = 0;
constexpr qint32 kEncCopyRect = 1; constexpr qint32 kEncCopyRect = 1;
constexpr qint32 kEncHextile = 5;
// RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content // RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content
// encoding -- a rectangle with this type carries a cursor shape update // encoding -- a rectangle with this type carries a cursor shape update
// instead (hotspot in x/y, image dims in width/height), never painted into // instead (hotspot in x/y, image dims in width/height), never painted into
// the framebuffer. // the framebuffer.
constexpr qint32 kEncCursor = -239; constexpr qint32 kEncCursor = -239;
constexpr std::array<qint32, 3> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncCursor }; constexpr std::array<qint32, 4> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncHextile,
kEncCursor };
quint16 readU16BE(const QByteArray& buf, int offset) quint16 readU16BE(const QByteArray& buf, int offset)
{ {
@@ -82,7 +86,14 @@ VncSessionBackend::VncSessionBackend(const Profile& profile, QObject* parent)
m_pendingRectanglesRemaining(0), m_pendingRectanglesRemaining(0),
m_pointerButtonMask(0), m_pointerButtonMask(0),
m_lastPointerX(0), m_lastPointerX(0),
m_lastPointerY(0) m_lastPointerY(0),
m_hextileTileX(0),
m_hextileTileY(0),
m_hextileBackground(qRgb(0, 0, 0)),
m_hextileForeground(qRgb(0, 0, 0)),
m_hextileSubencoding(0),
m_hextileSubrectsRemaining(0),
m_hextileSubrectsColoured(false)
{ {
connect(m_socket, &QTcpSocket::connected, this, &VncSessionBackend::onSocketConnected); connect(m_socket, &QTcpSocket::connected, this, &VncSessionBackend::onSocketConnected);
connect(m_socket, &QTcpSocket::readyRead, this, &VncSessionBackend::onSocketReadyRead); connect(m_socket, &QTcpSocket::readyRead, this, &VncSessionBackend::onSocketReadyRead);
@@ -326,6 +337,10 @@ void VncSessionBackend::onSocketDisconnected()
&& m_rfbState != RfbState::WaitingRawPixelData && m_rfbState != RfbState::WaitingRawPixelData
&& m_rfbState != RfbState::WaitingCopyRectSource && m_rfbState != RfbState::WaitingCopyRectSource
&& m_rfbState != RfbState::WaitingCursorPixelData && m_rfbState != RfbState::WaitingCursorPixelData
&& m_rfbState != RfbState::WaitingHextileTileSubencoding
&& m_rfbState != RfbState::WaitingHextileTileMeta
&& m_rfbState != RfbState::WaitingHextileSubrectData
&& m_rfbState != RfbState::WaitingHextileRawTileData
&& m_rfbState != RfbState::WaitingSetColourMapHeader && m_rfbState != RfbState::WaitingSetColourMapHeader
&& m_rfbState != RfbState::WaitingSetColourMapData && m_rfbState != RfbState::WaitingSetColourMapData
&& m_rfbState != RfbState::WaitingServerCutTextHeader && m_rfbState != RfbState::WaitingServerCutTextHeader
@@ -370,6 +385,10 @@ void VncSessionBackend::resetProtocolState()
m_framebuffer = QImage(); m_framebuffer = QImage();
m_pendingRectanglesRemaining = 0; m_pendingRectanglesRemaining = 0;
m_pointerButtonMask = 0; m_pointerButtonMask = 0;
m_hextileTileX = 0;
m_hextileTileY = 0;
m_hextileBackground = qRgb(0, 0, 0);
m_hextileForeground = qRgb(0, 0, 0);
} }
bool VncSessionBackend::haveBytes(int count) const bool VncSessionBackend::haveBytes(int count) const
@@ -516,6 +535,29 @@ void VncSessionBackend::onRectangleFinished()
requestFramebufferUpdate(true); requestFramebufferUpdate(true);
} }
QRect VncSessionBackend::currentHextileTileRect() const
{
const int tileWidth = qMin(16, m_currentRectangle.width - m_hextileTileX);
const int tileHeight = qMin(16, m_currentRectangle.height - m_hextileTileY);
return QRect(m_currentRectangle.x + m_hextileTileX, m_currentRectangle.y + m_hextileTileY,
tileWidth, tileHeight);
}
void VncSessionBackend::advanceHextileTile()
{
m_hextileTileX += 16;
if (m_hextileTileX >= m_currentRectangle.width) {
m_hextileTileX = 0;
m_hextileTileY += 16;
}
if (m_hextileTileY >= m_currentRectangle.height) {
onRectangleFinished();
} else {
m_rfbState = RfbState::WaitingHextileTileSubencoding;
}
}
void VncSessionBackend::processReceiveBuffer() void VncSessionBackend::processReceiveBuffer()
{ {
for (;;) { for (;;) {
@@ -819,6 +861,18 @@ void VncSessionBackend::processReceiveBuffer()
case kEncCursor: case kEncCursor:
m_rfbState = RfbState::WaitingCursorPixelData; m_rfbState = RfbState::WaitingCursorPixelData;
break; break;
case kEncHextile:
m_hextileTileX = 0;
m_hextileTileY = 0;
m_hextileBackground = qRgb(0, 0, 0);
m_hextileForeground = qRgb(0, 0, 0);
if (m_currentRectangle.width <= 0 || m_currentRectangle.height <= 0) {
// No tiles to decode at all.
onRectangleFinished();
} else {
m_rfbState = RfbState::WaitingHextileTileSubencoding;
}
break;
default: { default: {
// SetEncodings (see kAnnouncedEncodings) is entirely // SetEncodings (see kAnnouncedEncodings) is entirely
// client-controlled, so a spec-compliant server will never // client-controlled, so a spec-compliant server will never
@@ -941,6 +995,94 @@ void VncSessionBackend::processReceiveBuffer()
break; break;
} }
case RfbState::WaitingHextileTileSubencoding: {
if (!haveBytes(1)) {
return;
}
m_hextileSubencoding = static_cast<quint8>(m_recvBuffer.at(0));
m_recvBuffer.remove(0, 1);
if ((m_hextileSubencoding & VncPixelCodecs::HextileFlags::kRaw) != 0) {
m_rfbState = RfbState::WaitingHextileRawTileData;
} else {
m_pendingLength = static_cast<quint32>(
VncPixelCodecs::hextileFixedMetaByteCount(m_hextileSubencoding));
m_rfbState = RfbState::WaitingHextileTileMeta;
}
break;
}
case RfbState::WaitingHextileTileMeta: {
if (!haveBytes(static_cast<int>(m_pendingLength))) {
return;
}
const QByteArray data = m_recvBuffer.left(static_cast<int>(m_pendingLength));
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
const int subrectCount = VncPixelCodecs::decodeHextileFixedMeta(
m_hextileSubencoding, data, &m_hextileBackground, &m_hextileForeground);
// Every tile is filled with the (possibly just-updated,
// possibly inherited) background colour first, regardless of
// whether this tile re-specified it.
QPainter painter(&m_framebuffer);
painter.fillRect(currentHextileTileRect(), QColor::fromRgb(m_hextileBackground));
m_hextileSubrectsColoured =
(m_hextileSubencoding & VncPixelCodecs::HextileFlags::kSubrectsColoured) != 0;
m_hextileSubrectsRemaining = subrectCount;
if (subrectCount == 0) {
advanceHextileTile();
} else {
m_pendingLength = static_cast<quint32>(VncPixelCodecs::hextileSubrectByteCount(
m_hextileSubrectsColoured, subrectCount));
m_rfbState = RfbState::WaitingHextileSubrectData;
}
break;
}
case RfbState::WaitingHextileSubrectData: {
if (!haveBytes(static_cast<int>(m_pendingLength))) {
return;
}
const QByteArray data = m_recvBuffer.left(static_cast<int>(m_pendingLength));
m_recvBuffer.remove(0, static_cast<int>(m_pendingLength));
const QVector<VncPixelCodecs::HextileSubrect> subrects =
VncPixelCodecs::decodeHextileSubrects(m_hextileSubrectsColoured,
m_hextileSubrectsRemaining, data,
m_hextileForeground);
const QRect tileRect = currentHextileTileRect();
QPainter painter(&m_framebuffer);
for (const VncPixelCodecs::HextileSubrect& subrect : subrects) {
painter.fillRect(subrect.rect.translated(tileRect.topLeft()),
QColor::fromRgb(subrect.color));
}
advanceHextileTile();
break;
}
case RfbState::WaitingHextileRawTileData: {
const QRect tileRect = currentHextileTileRect();
const qint64 byteCount =
static_cast<qint64>(tileRect.width()) * tileRect.height() * 4;
if (!haveBytes(static_cast<int>(byteCount))) {
return;
}
if (byteCount > 0) {
const QImage tileImage(reinterpret_cast<const uchar*>(m_recvBuffer.constData()),
tileRect.width(), tileRect.height(),
tileRect.width() * 4, QImage::Format_RGB32);
QPainter painter(&m_framebuffer);
painter.drawImage(tileRect.topLeft(), tileImage);
}
m_recvBuffer.remove(0, static_cast<int>(byteCount));
advanceHextileTile();
break;
}
case RfbState::WaitingSetColourMapHeader: { case RfbState::WaitingSetColourMapHeader: {
if (!haveBytes(5)) { if (!haveBytes(5)) {
return; return;
+25 -4
View File
@@ -6,6 +6,8 @@
#include <QAbstractSocket> #include <QAbstractSocket>
#include <QByteArray> #include <QByteArray>
#include <QImage> #include <QImage>
#include <QRect>
#include <QRgb>
class QTcpSocket; class QTcpSocket;
@@ -20,10 +22,11 @@ class QTcpSocket;
// //
// Scope (see plan / issue #3 for the full rationale): standard VNC // Scope (see plan / issue #3 for the full rationale): standard VNC
// Authentication (security type 2) and no-auth (type 1) only -- not // Authentication (security type 2) and no-auth (type 1) only -- not
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect encodings only. // Apple's Screen Sharing scheme (type 30). Raw + CopyRect + Hextile
// No dynamic resize. Clipboard sync (Latin-1 only, per RFB's // encodings (ZRLE/Tight compression not yet implemented). No dynamic
// ServerCutText/ClientCutText) and remote cursor shape sync (the Cursor // resize. Clipboard sync (Latin-1 only, per RFB's ServerCutText/
// pseudo-encoding) are supported. // ClientCutText) and remote cursor shape sync (the Cursor pseudo-encoding)
// are supported.
class VncSessionBackend : public SessionBackend class VncSessionBackend : public SessionBackend
{ {
Q_OBJECT Q_OBJECT
@@ -83,6 +86,10 @@ private:
WaitingRawPixelData, WaitingRawPixelData,
WaitingCopyRectSource, WaitingCopyRectSource,
WaitingCursorPixelData, WaitingCursorPixelData,
WaitingHextileTileSubencoding,
WaitingHextileTileMeta,
WaitingHextileSubrectData,
WaitingHextileRawTileData,
WaitingSetColourMapHeader, WaitingSetColourMapHeader,
WaitingSetColourMapData, WaitingSetColourMapData,
WaitingServerCutTextHeader, WaitingServerCutTextHeader,
@@ -120,6 +127,18 @@ private:
int m_lastPointerX; int m_lastPointerX;
int m_lastPointerY; int m_lastPointerY;
// Hextile decode state (RFC 6143 SS7.7.4): tile-cursor position relative
// to the current rectangle's origin, plus the background/foreground
// colors, which persist across tiles within one rectangle whenever a
// tile doesn't re-specify them.
int m_hextileTileX;
int m_hextileTileY;
QRgb m_hextileBackground;
QRgb m_hextileForeground;
quint8 m_hextileSubencoding;
int m_hextileSubrectsRemaining;
bool m_hextileSubrectsColoured;
void setState(SessionState state, const QString& message); void setState(SessionState state, const QString& message);
void resetProtocolState(); void resetProtocolState();
void processReceiveBuffer(); void processReceiveBuffer();
@@ -134,6 +153,8 @@ private:
void sendPointerEvent(); void sendPointerEvent();
void sendWheelClick(quint8 wheelBit); void sendWheelClick(quint8 wheelBit);
void sendClientCutText(const QString& text); void sendClientCutText(const QString& text);
QRect currentHextileTileRect() const;
void advanceHextileTile();
}; };
#endif #endif
+1
View File
@@ -29,6 +29,7 @@ add_test(NAME test_ssh_session_backend COMMAND test_ssh_session_backend)
add_executable(test_vnc_session_backend add_executable(test_vnc_session_backend
test_vnc_session_backend.cpp test_vnc_session_backend.cpp
${CMAKE_SOURCE_DIR}/src/vnc_session_backend.cpp ${CMAKE_SOURCE_DIR}/src/vnc_session_backend.cpp
${CMAKE_SOURCE_DIR}/src/vnc_pixel_codecs.cpp
${CMAKE_SOURCE_DIR}/src/session_backend.h ${CMAKE_SOURCE_DIR}/src/session_backend.h
) )
target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src) target_include_directories(test_vnc_session_backend PRIVATE ${CMAKE_SOURCE_DIR}/src)
+281
View File
@@ -79,6 +79,52 @@ SessionConnectOptions makeOptions(const QString& password = QString())
options.password = password; options.password = password;
return options; return options;
} }
void appendU16(QByteArray& buf, quint16 value)
{
buf.append(static_cast<char>((value >> 8) & 0xFF));
buf.append(static_cast<char>(value & 0xFF));
}
// Pixel bytes as expected under our negotiated SetPixelFormat: 32bpp
// little-endian, byte order B,G,R,pad.
QByteArray pixelBytes(int r, int g, int b)
{
QByteArray bytes;
bytes.append(static_cast<char>(b));
bytes.append(static_cast<char>(g));
bytes.append(static_cast<char>(r));
bytes.append(char(0));
return bytes;
}
QByteArray serverInitBytes(int width, int height)
{
QByteArray serverInit;
appendU16(serverInit, static_cast<quint16>(width));
appendU16(serverInit, static_cast<quint16>(height));
serverInit.append(QByteArray(16, char(0)));
serverInit.append(QByteArray(4, char(0)));
return serverInit;
}
// A FramebufferUpdate message with exactly one Hextile-encoded rectangle
// (RFC 6143 encoding type 5), given the already fully-formed tile byte
// stream to follow the rectangle header.
QByteArray hextileFramebufferUpdate(int x, int y, int width, int height, const QByteArray& tileData)
{
QByteArray update;
update.append(char(0)); update.append(char(0));
update.append(char(0)); update.append(char(1)); // 1 rectangle
appendU16(update, static_cast<quint16>(x));
appendU16(update, static_cast<quint16>(y));
appendU16(update, static_cast<quint16>(width));
appendU16(update, static_cast<quint16>(height));
update.append(char(0)); update.append(char(0)); update.append(char(0));
update.append(char(5)); // encoding = Hextile
update.append(tileData);
return update;
}
} }
class TestVncSessionBackend : public QObject class TestVncSessionBackend : public QObject
@@ -114,6 +160,11 @@ private slots:
void setClipboardTextSendsClientCutText(); void setClipboardTextSendsClientCutText();
void cursorPseudoEncodingProducesExpectedImageAndHotspot(); void cursorPseudoEncodingProducesExpectedImageAndHotspot();
void zeroSizeCursorPseudoEncodingHidesCursor(); void zeroSizeCursorPseudoEncodingHidesCursor();
void hextileRawTileProducesExpectedPixels();
void hextileBackgroundOnlyTileFillsSolidColor();
void hextileUncolouredSubrectUsesForeground();
void hextileColouredSubrectUsesOwnColor();
void hextileMultiTilePersistsBackgroundAcrossTiles();
private: private:
std::unique_ptr<FakeVncServer> m_server; std::unique_ptr<FakeVncServer> m_server;
@@ -843,5 +894,235 @@ void TestVncSessionBackend::zeroSizeCursorPseudoEncodingHidesCursor()
QTRY_VERIFY(gotHidden); QTRY_VERIFY(gotHidden);
} }
void TestVncSessionBackend::hextileRawTileProducesExpectedPixels()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(2, 2));
break;
case 3: { // a single Raw-subencoding Hextile tile covering the whole 2x2 rectangle
QByteArray tile;
tile.append(char(0x01)); // HextileFlags::kRaw
tile += pixelBytes(255, 0, 0);
tile += pixelBytes(0, 255, 0);
tile += pixelBytes(0, 0, 255);
tile += pixelBytes(255, 255, 255);
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 2, 2, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(2, 2));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 0), QColor(0, 255, 0));
QCOMPARE(m_lastFrame.pixelColor(0, 1), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 255, 255));
}
void TestVncSessionBackend::hextileBackgroundOnlyTileFillsSolidColor()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(3, 3));
break;
case 3: { // BackgroundSpecified only, no subrects -- solid fill
QByteArray tile;
tile.append(char(0x02)); // HextileFlags::kBackgroundSpecified
tile += pixelBytes(0, 128, 255);
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
for (int y = 0; y < 3; ++y) {
for (int x = 0; x < 3; ++x) {
QCOMPARE(m_lastFrame.pixelColor(x, y), QColor(0, 128, 255));
}
}
}
void TestVncSessionBackend::hextileUncolouredSubrectUsesForeground()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(4, 4));
break;
case 3: {
// Background(black) + Foreground(red) + one uncoloured 2x2
// subrect at tile-local (1,1) -- must render in the foreground
// color, everywhere else in the background color.
QByteArray tile;
tile.append(char(0x02 | 0x04 | 0x08)); // Background|Foreground|AnySubrects
tile += pixelBytes(0, 0, 0); // background
tile += pixelBytes(255, 0, 0); // foreground
tile.append(char(1)); // subrect count
tile.append(char(0x11)); // xy: x=1, y=1
tile.append(char(0x11)); // wh: width=2, height=2
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 4, 4, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(4, 4));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(255, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(3, 3), QColor(0, 0, 0));
}
void TestVncSessionBackend::hextileColouredSubrectUsesOwnColor()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(3, 3));
break;
case 3: {
// No background/foreground specified this tile -- both stay at
// the rectangle-start default (black). One individually-
// coloured 1x1 subrect at tile-local (0,0).
QByteArray tile;
tile.append(char(0x08 | 0x10)); // AnySubrects|SubrectsColoured
tile.append(char(1)); // subrect count
tile += pixelBytes(0, 255, 0); // this subrect's own color
tile.append(char(0x00)); // xy: x=0, y=0
tile.append(char(0x00)); // wh: width=1, height=1
m_server->sendWhenConnected(hextileFramebufferUpdate(0, 0, 3, 3, tile));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(3, 3));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 255, 0));
QCOMPARE(m_lastFrame.pixelColor(1, 1), QColor(0, 0, 0));
QCOMPARE(m_lastFrame.pixelColor(2, 2), QColor(0, 0, 0));
}
void TestVncSessionBackend::hextileMultiTilePersistsBackgroundAcrossTiles()
{
connect(m_server.get(), &FakeVncServer::clientConnected, this, [this]() {
m_server->sendWhenConnected(QByteArray("RFB 003.008\n"));
});
connect(m_server.get(), &FakeVncServer::dataReceived, this, [this]() {
switch (m_server->nextStep()) {
case 0: {
QByteArray securityTypes;
securityTypes.append(char(1));
securityTypes.append(char(1));
m_server->sendWhenConnected(securityTypes);
break;
}
case 1:
m_server->sendWhenConnected(QByteArray(4, char(0))); // SecurityResult: OK
break;
case 2:
m_server->sendWhenConnected(serverInitBytes(20, 20));
break;
case 3: {
// A 20x20 rectangle tiles as (0,0) 16x16, (16,0) 4x16,
// (0,16) 16x4, (16,16) 4x4 -- only the first tile specifies a
// background color; the other three specify nothing at all
// (subencoding 0x00) and must inherit it.
QByteArray tile1;
tile1.append(char(0x02)); // BackgroundSpecified
tile1 += pixelBytes(0, 0, 255);
QByteArray tile2(1, char(0x00));
QByteArray tile3(1, char(0x00));
QByteArray tile4(1, char(0x00));
m_server->sendWhenConnected(
hextileFramebufferUpdate(0, 0, 20, 20, tile1 + tile2 + tile3 + tile4));
break;
}
default:
break;
}
});
m_backend->connectSession(makeOptions());
QTRY_VERIFY(m_gotFrame);
QCOMPARE(m_lastFrame.size(), QSize(20, 20));
QCOMPARE(m_lastFrame.pixelColor(0, 0), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(19, 0), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(0, 19), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(19, 19), QColor(0, 0, 255));
QCOMPARE(m_lastFrame.pixelColor(10, 10), QColor(0, 0, 255));
}
QTEST_GUILESS_MAIN(TestVncSessionBackend) QTEST_GUILESS_MAIN(TestVncSessionBackend)
#include "test_vnc_session_backend.moc" #include "test_vnc_session_backend.moc"