Add VNC Tight decoding

Implements RFC 6143's Tight encoding: a compression-control byte (low
4 bits reset one of 4 persistent zlib streams; high nibble selects
Fill/JPEG/Basic mode) followed by Fill's 3-byte solid color, JPEG's
compact-length-prefixed baseline JPEG covering the whole rectangle
(decoded via libjpeg-turbo directly, not QImage's plugin, to avoid a
packaging-dependent runtime failure mode), or Basic mode's
compact-length-prefixed zlib payload plus a filter (Copy, Palette, or
Gradient) applied after decompression. Unlike Hextile/ZRLE, Tight has
no internal tiling -- one rectangle is one filtered/compressed unit.

The three filters live in vnc_pixel_codecs.h/.cpp alongside the
Hextile/ZRLE decoders. Adds find_package(JPEG REQUIRED) + JPEG::JPEG
as a new build dependency (confirmed available via libjpeg-turbo on
this dev machine). 5 new tests cover Fill, Basic+Copy, Basic+Palette,
JPEG (round-tripped through a real libjpeg-turbo-encoded fixture,
compared with tolerance since JPEG is lossy), and the stream-reset
flag correctly tearing down and reinitializing a targeted stream
rather than erroring on stale state.

Known, documented gap: this decoder always treats Basic-mode payloads
as zlib-compressed; the real protocol allows very small payloads to
skip compression, which couldn't be verified with confidence against
the RFC text alone and is narrow enough in practice (tiny solid areas
are virtually always sent as Fill instead) to leave unhandled for now
-- it fails that one rectangle's decode cleanly rather than
misinterpreting it silently. The Gradient filter is implemented from
the spec description but is the least exercised of the three in this
pass.

Live-verified against the TightVNC test server that nothing
regressed; that server still consistently chose Raw for actual
framebuffer content regardless of announced encodings, so Tight's live
decode path isn't independently confirmed against a real server here
either -- the unit tests are the primary evidence.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 21:13:17 -06:00
co-authored by Claude Sonnet 5
parent bb022edcf2
commit 4fca8fce41
8 changed files with 901 additions and 26 deletions
+32 -3
View File
@@ -9,6 +9,8 @@
#include <QRect>
#include <QRgb>
#include <array>
class QTcpSocket;
struct z_stream_s;
@@ -24,9 +26,19 @@ struct z_stream_s;
// Scope (see plan / issue #3 for the full rationale): standard VNC
// Authentication (security type 2) and no-auth (type 1) only -- not
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect + Hextile + ZRLE
// encodings (Tight compression not yet implemented). No dynamic resize.
// Clipboard sync (Latin-1 only, per RFB's ServerCutText/ClientCutText) and
// remote cursor shape sync (the Cursor pseudo-encoding) are supported.
// + Tight encodings. No dynamic resize. Clipboard sync (Latin-1 only, per
// RFB's ServerCutText/ClientCutText) and remote cursor shape sync (the
// Cursor pseudo-encoding) are supported.
//
// Tight decoding gap: the real protocol allows the server to skip zlib
// compression entirely for very small Basic-mode payloads; this decoder
// always attempts to zlib-inflate them, so a server that takes that
// shortcut on a given rectangle would have that one rectangle fail rather
// than decode. This is intentionally not special-cased (the exact trigger
// condition/wire signaling for it could not be verified with confidence
// against the RFC text alone, and it only affects rare, tiny rectangles --
// solid or near-solid tiny areas are virtually always sent as Fill instead
// in practice) -- see docs/PROGRESS.md.
class VncSessionBackend : public SessionBackend
{
Q_OBJECT
@@ -92,6 +104,11 @@ private:
WaitingHextileRawTileData,
WaitingZrleCompressedLength,
WaitingZrleCompressedData,
WaitingTightCompressionControl,
WaitingTightFillColor,
WaitingTightFilterId,
WaitingTightLengthByte,
WaitingTightPayload,
WaitingSetColourMapHeader,
WaitingSetColourMapData,
WaitingServerCutTextHeader,
@@ -150,6 +167,17 @@ private:
z_stream_s* m_zrleInflateStream;
bool m_zrleInflateInitialized;
// Tight decode state (RFC 6143 SS7.7.4). Unlike ZRLE, Tight's "Basic"
// compression mode has 4 independent persistent zlib streams (chosen
// per-rectangle by 2 bits of the compression-control byte), each with
// its own lifecycle -- reset individually via the control byte's low 4
// bits, otherwise persisting like ZRLE's single stream.
std::array<z_stream_s*, 4> m_tightInflateStreams;
std::array<bool, 4> m_tightInflateInitialized;
quint8 m_tightCompressionMode; // compression-control byte >> 4
quint8 m_tightFilterId;
int m_tightLengthByteIndex;
void setState(SessionState state, const QString& message);
void resetProtocolState();
void processReceiveBuffer();
@@ -166,6 +194,7 @@ private:
void sendClientCutText(const QString& text);
QRect currentHextileTileRect() const;
void advanceHextileTile();
bool inflateTightStream(int streamIndex, const QByteArray& compressed, QByteArray* decompressed);
};
#endif