Lock pixel format to 32bpp little-endian truecolour and reject other formats

This commit is contained in:
Viktor Liu
2026-05-17 08:54:58 +02:00
parent b4f696272a
commit 2bed8b641b
3 changed files with 56 additions and 148 deletions

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"compress/zlib" "compress/zlib"
"encoding/binary" "encoding/binary"
"fmt"
"image" "image"
"image/jpeg" "image/jpeg"
"unsafe" "unsafe"
@@ -80,24 +81,17 @@ const (
// Distinct-colour cap below which we still prefer Basic+zlib (text, // Distinct-colour cap below which we still prefer Basic+zlib (text,
// UI). Sampled, not exhaustive: cheap to compute, good enough. // UI). Sampled, not exhaustive: cheap to compute, good enough.
tightJPEGMinColors = 64 tightJPEGMinColors = 64
// Hextile subencoding flags (a bitmask in the first byte of each sub-tile).
hextileRaw = 0x01
hextileBackgroundSpecified = 0x02
hextileForegroundSpecified = 0x04
hextileAnySubrects = 0x08
hextileSubrectsColoured = 0x10
// Hextile sub-tile size per RFB spec.
hextileSubSize = 16
) )
// serverPixelFormat is the default pixel format advertised by the server: // serverPixelFormat is the pixel format the server advertises and requires:
// 32bpp RGBA, big-endian, true-colour, 8 bits per channel. // 32bpp RGBA, little-endian, true-colour, 8 bits per channel at standard
// shifts (R=16, G=8, B=0). handleSetPixelFormat rejects any client that
// negotiates a different format. Browser-side decoders are little-endian
// natively, so advertising little-endian skips a byte-swap on every pixel.
var serverPixelFormat = [16]byte{ var serverPixelFormat = [16]byte{
32, // bits-per-pixel 32, // bits-per-pixel
24, // depth 24, // depth
1, // big-endian-flag 0, // big-endian-flag
1, // true-colour-flag 1, // true-colour-flag
0, 255, // red-max 0, 255, // red-max
0, 255, // green-max 0, 255, // green-max
@@ -108,42 +102,48 @@ var serverPixelFormat = [16]byte{
0, 0, 0, // padding 0, 0, 0, // padding
} }
// clientPixelFormat holds the negotiated pixel format from the client. // clientPixelFormat holds the negotiated pixel format. Only RGB channel
// shifts are tracked: every other field is constrained by the server to
// the values in serverPixelFormat (32bpp / little-endian / truecolour /
// 8-bit channels) and rejected at SetPixelFormat time if the client tries
// to negotiate otherwise.
type clientPixelFormat struct { type clientPixelFormat struct {
bpp uint8 rShift uint8
bigEndian uint8 gShift uint8
rMax uint16 bShift uint8
gMax uint16
bMax uint16
rShift uint8
gShift uint8
bShift uint8
} }
func defaultClientPixelFormat() clientPixelFormat { func defaultClientPixelFormat() clientPixelFormat {
return clientPixelFormat{ return clientPixelFormat{
bpp: serverPixelFormat[0], rShift: serverPixelFormat[10],
bigEndian: serverPixelFormat[2], gShift: serverPixelFormat[11],
rMax: binary.BigEndian.Uint16(serverPixelFormat[4:6]), bShift: serverPixelFormat[12],
gMax: binary.BigEndian.Uint16(serverPixelFormat[6:8]),
bMax: binary.BigEndian.Uint16(serverPixelFormat[8:10]),
rShift: serverPixelFormat[10],
gShift: serverPixelFormat[11],
bShift: serverPixelFormat[12],
} }
} }
func parsePixelFormat(pf []byte) clientPixelFormat { // parsePixelFormat returns the negotiated client pixel format, or an error
return clientPixelFormat{ // if the client tried to negotiate an unsupported format. The server only
bpp: pf[0], // supports 32bpp truecolour little-endian with 8-bit channels; arbitrary
bigEndian: pf[2], // shifts within that constraint are allowed because they are cheap to honour.
rMax: binary.BigEndian.Uint16(pf[4:6]), func parsePixelFormat(pf []byte) (clientPixelFormat, error) {
gMax: binary.BigEndian.Uint16(pf[6:8]), bpp := pf[0]
bMax: binary.BigEndian.Uint16(pf[8:10]), bigEndian := pf[2]
rShift: pf[10], trueColour := pf[3]
gShift: pf[11], rMax := binary.BigEndian.Uint16(pf[4:6])
bShift: pf[12], gMax := binary.BigEndian.Uint16(pf[6:8])
bMax := binary.BigEndian.Uint16(pf[8:10])
if bpp != 32 || bigEndian != 0 || trueColour != 1 ||
rMax != 255 || gMax != 255 || bMax != 255 {
return clientPixelFormat{}, fmt.Errorf(
"unsupported pixel format (bpp=%d be=%d tc=%d rgb-max=%d/%d/%d): "+
"server only supports 32bpp truecolour little-endian 8-bit channels",
bpp, bigEndian, trueColour, rMax, gMax, bMax)
} }
return clientPixelFormat{
rShift: pf[10],
gShift: pf[11],
bShift: pf[12],
}, nil
} }
// encodeCopyRectBody emits the per-rect payload for a CopyRect rectangle: // encodeCopyRectBody emits the per-rect payload for a CopyRect rectangle:
@@ -211,10 +211,7 @@ func encodeLastRectBody() []byte {
// encodeRawRect encodes a framebuffer region as a raw RFB rectangle. // encodeRawRect encodes a framebuffer region as a raw RFB rectangle.
// The returned buffer includes the FramebufferUpdate header (1 rectangle). // The returned buffer includes the FramebufferUpdate header (1 rectangle).
func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte { func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte {
bytesPerPixel := max(int(pf.bpp)/8, 1) buf := make([]byte, 4+12+w*h*4)
pixelBytes := w * h * bytesPerPixel
buf := make([]byte, 4+12+pixelBytes)
// FramebufferUpdate header. // FramebufferUpdate header.
buf[0] = serverFramebufferUpdate buf[0] = serverFramebufferUpdate
@@ -228,26 +225,17 @@ func encodeRawRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte
binary.BigEndian.PutUint16(buf[10:12], uint16(h)) binary.BigEndian.PutUint16(buf[10:12], uint16(h))
binary.BigEndian.PutUint32(buf[12:16], uint32(encRaw)) binary.BigEndian.PutUint32(buf[12:16], uint32(encRaw))
writePixels(buf[16:], img, pf, rect{x, y, w, h}, bytesPerPixel) writePixels(buf[16:], img, pf, rect{x, y, w, h})
return buf return buf
} }
// writePixels writes a rectangle of img into dst in the client's requested // writePixels writes a rectangle of img into dst as 32bpp little-endian
// pixel format. It fast-paths the common case (32bpp, full 8-bit channels) // pixels at the negotiated RGB shifts. The pixel format is constrained at
// with a tight loop that skips the per-channel *max/255 arithmetic and emits // SetPixelFormat time so we can assume 4 bytes per pixel, 8-bit channels,
// a single uint32 per pixel; the general path handles arbitrary formats. // and little-endian byte order; arbitrary shifts (R/G/B order) are honoured.
func writePixels(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect, bytesPerPixel int) { func writePixels(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect) {
if bytesPerPixel == 4 && pf.rMax == 255 && pf.gMax == 255 && pf.bMax == 255 {
writePixelsFast32(dst, img, pf, r)
return
}
writePixelsGeneric(dst, img, pf, r, bytesPerPixel)
}
func writePixelsFast32(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect) {
stride := img.Stride stride := img.Stride
rShift, gShift, bShift := pf.rShift, pf.gShift, pf.bShift rShift, gShift, bShift := pf.rShift, pf.gShift, pf.bShift
bigEndian := pf.bigEndian != 0
off := 0 off := 0
for row := r.y; row < r.y+r.h; row++ { for row := r.y; row < r.y+r.h; row++ {
p := row*stride + r.x*4 p := row*stride + r.x*4
@@ -255,47 +243,13 @@ func writePixelsFast32(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect
pixel := (uint32(img.Pix[p]) << rShift) | pixel := (uint32(img.Pix[p]) << rShift) |
(uint32(img.Pix[p+1]) << gShift) | (uint32(img.Pix[p+1]) << gShift) |
(uint32(img.Pix[p+2]) << bShift) (uint32(img.Pix[p+2]) << bShift)
if bigEndian { binary.LittleEndian.PutUint32(dst[off:off+4], pixel)
binary.BigEndian.PutUint32(dst[off:off+4], pixel)
} else {
binary.LittleEndian.PutUint32(dst[off:off+4], pixel)
}
p += 4 p += 4
off += 4 off += 4
} }
} }
} }
func writePixelsGeneric(dst []byte, img *image.RGBA, pf clientPixelFormat, r rect, bytesPerPixel int) {
stride := img.Stride
off := 0
for row := r.y; row < r.y+r.h; row++ {
for col := r.x; col < r.x+r.w; col++ {
p := row*stride + col*4
rv := uint32(img.Pix[p]) * uint32(pf.rMax) / 255
gv := uint32(img.Pix[p+1]) * uint32(pf.gMax) / 255
bv := uint32(img.Pix[p+2]) * uint32(pf.bMax) / 255
pixel := (rv << pf.rShift) | (gv << pf.gShift) | (bv << pf.bShift)
emitPixelBytes(dst[off:off+bytesPerPixel], pixel, bytesPerPixel, pf.bigEndian != 0)
off += bytesPerPixel
}
}
}
func emitPixelBytes(dst []byte, pixel uint32, bytesPerPixel int, bigEndian bool) {
if bigEndian {
for i := range bytesPerPixel {
dst[i] = byte(pixel >> uint((bytesPerPixel-1-i)*8))
}
return
}
for i := range bytesPerPixel {
dst[i] = byte(pixel >> uint(i*8))
}
}
// diffTiles compares two RGBA images and returns a tile-ordered list of // diffTiles compares two RGBA images and returns a tile-ordered list of
// dirty tiles, one entry per tile. Tile order is top-to-bottom, left-to- // dirty tiles, one entry per tile. Tile order is top-to-bottom, left-to-
// right within each row. The caller decides whether to coalesce or hand // right within each row. The caller decides whether to coalesce or hand
@@ -453,33 +407,6 @@ func tileIsUniform(img *image.RGBA, x, y, w, h int) (uint32, bool) {
return first, true return first, true
} }
// encodePixel packs an RGBA byte triple into the client's requested pixel
// format, honouring bpp, channel maxes, shifts and endianness. Returns the
// number of bytes written to dst (1..4).
func encodePixel(dst []byte, pf clientPixelFormat, r, g, b byte) int {
bytesPerPixel := max(int(pf.bpp)/8, 1)
var val uint32
if pf.rMax == 255 && pf.gMax == 255 && pf.bMax == 255 {
val = (uint32(r) << pf.rShift) | (uint32(g) << pf.gShift) | (uint32(b) << pf.bShift)
} else {
rv := uint32(r) * uint32(pf.rMax) / 255
gv := uint32(g) * uint32(pf.gMax) / 255
bv := uint32(b) * uint32(pf.bMax) / 255
val = (rv << pf.rShift) | (gv << pf.gShift) | (bv << pf.bShift)
}
if pf.bigEndian != 0 {
for i := range bytesPerPixel {
dst[i] = byte(val >> uint((bytesPerPixel-1-i)*8))
}
} else {
for i := range bytesPerPixel {
dst[i] = byte(val >> uint(i*8))
}
}
return bytesPerPixel
}
// tightState holds the per-session JPEG scratch buffer and reused encoders // tightState holds the per-session JPEG scratch buffer and reused encoders
// so per-rect encoding stays alloc-free in the steady state. // so per-rect encoding stays alloc-free in the steady state.
type tightState struct { type tightState struct {

View File

@@ -84,26 +84,7 @@ func BenchmarkWritePixels(b *testing.B) {
b.SetBytes(int64(r.w * r.h * 4)) b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
writePixels(dst, img, pf, rect{0, 0, r.w, r.h}, 4) writePixels(dst, img, pf, rect{0, 0, r.w, r.h})
}
})
}
}
// BenchmarkWritePixelsScaled forces the general (non-fast) path by using a
// pixel format with non-255 channel maxes.
func BenchmarkWritePixelsScaled(b *testing.B) {
pf := defaultClientPixelFormat()
pf.rMax, pf.gMax, pf.bMax = 31, 63, 31 // 16bpp-ish; exercises the divide path
pf.bpp = 16
for _, r := range benchRects {
img := makeBenchImage(r.w, r.h, 1)
dst := make([]byte, r.w*r.h*2)
b.Run(r.name, func(b *testing.B) {
b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs()
for i := 0; i < b.N; i++ {
writePixels(dst, img, pf, rect{0, 0, r.w, r.h}, 2)
} }
}) })
} }

View File

@@ -260,7 +260,10 @@ func (s *session) handleSetPixelFormat() error {
if _, err := io.ReadFull(s.conn, buf[:]); err != nil { if _, err := io.ReadFull(s.conn, buf[:]); err != nil {
return fmt.Errorf("read SetPixelFormat: %w", err) return fmt.Errorf("read SetPixelFormat: %w", err)
} }
pf := parsePixelFormat(buf[3:19]) pf, err := parsePixelFormat(buf[3:19])
if err != nil {
return err
}
s.encMu.Lock() s.encMu.Lock()
s.pf = pf s.pf = pf
s.encMu.Unlock() s.encMu.Unlock()
@@ -822,11 +825,8 @@ func drainRequests(ch chan fbRequest) int {
} }
// pfIsTightCompatible reports whether the negotiated client pixel format // pfIsTightCompatible reports whether the negotiated client pixel format
// matches Tight's TPIXEL constraint: 32 bpp true colour with 8-bit RGB // matches Tight's TPIXEL constraint: standard RGB shifts (R=16, G=8, B=0).
// channels at standard shifts (R=16, G=8, B=0). For anything else we fall // bpp/endianness/channel-max are already locked at SetPixelFormat time.
// back to Zlib/Hextile/Raw which respect pf in full.
func pfIsTightCompatible(pf clientPixelFormat) bool { func pfIsTightCompatible(pf clientPixelFormat) bool {
return pf.bpp == 32 && return pf.rShift == 16 && pf.gShift == 8 && pf.bShift == 0
pf.rMax == 255 && pf.gMax == 255 && pf.bMax == 255 &&
pf.rShift == 16 && pf.gShift == 8 && pf.bShift == 0
} }