Drop dead Hextile and standalone Zlib encoding paths

This commit is contained in:
Viktor Liu
2026-05-17 08:48:21 +02:00
parent db5b6cfbb7
commit 6d937af7a0
5 changed files with 71 additions and 545 deletions

View File

@@ -1,188 +0,0 @@
package server
import (
"image"
"testing"
)
// roundTrip decodes an encoded Hextile rect back into pixels and checks it
// matches the source. Implements just enough of the noVNC Hextile decoder
// to validate our encoder.
func decodeHextile(t *testing.T, buf []byte, pf clientPixelFormat) *image.RGBA {
t.Helper()
if len(buf) < 12 {
t.Fatalf("buf too short: %d", len(buf))
}
x := int(uint16(buf[0])<<8 | uint16(buf[1]))
y := int(uint16(buf[2])<<8 | uint16(buf[3]))
w := int(uint16(buf[4])<<8 | uint16(buf[5]))
h := int(uint16(buf[6])<<8 | uint16(buf[7]))
enc := uint32(buf[8])<<24 | uint32(buf[9])<<16 | uint32(buf[10])<<8 | uint32(buf[11])
if enc != encHextile {
t.Fatalf("not hextile: %d", enc)
}
body := buf[12:]
bytesPerPixel := max(int(pf.bpp)/8, 1)
out := image.NewRGBA(image.Rect(x, y, x+w, y+h))
var bg, fg [3]byte
pos := 0
readPixel := func() [3]byte {
var v uint32
if pf.bigEndian != 0 {
for i := 0; i < bytesPerPixel; i++ {
v |= uint32(body[pos+i]) << (8 * (bytesPerPixel - 1 - i))
}
} else {
for i := 0; i < bytesPerPixel; i++ {
v |= uint32(body[pos+i]) << (8 * i)
}
}
pos += bytesPerPixel
r := byte((v >> pf.rShift) & uint32(pf.rMax))
g := byte((v >> pf.gShift) & uint32(pf.gMax))
b := byte((v >> pf.bShift) & uint32(pf.bMax))
return [3]byte{r, g, b}
}
for sy := 0; sy < h; sy += hextileSubSize {
sh := min(hextileSubSize, h-sy)
for sx := 0; sx < w; sx += hextileSubSize {
sw := min(hextileSubSize, w-sx)
flags := body[pos]
pos++
if flags&hextileRaw != 0 {
for ry := 0; ry < sh; ry++ {
for rx := 0; rx < sw; rx++ {
px := readPixel()
i := (sy+ry)*out.Stride + (sx+rx)*4
out.Pix[i+0] = px[0]
out.Pix[i+1] = px[1]
out.Pix[i+2] = px[2]
out.Pix[i+3] = 0xff
}
}
continue
}
if flags&hextileBackgroundSpecified != 0 {
bg = readPixel()
}
if flags&hextileForegroundSpecified != 0 {
fg = readPixel()
}
// Fill sub-tile with bg.
for ry := 0; ry < sh; ry++ {
for rx := 0; rx < sw; rx++ {
i := (sy+ry)*out.Stride + (sx+rx)*4
out.Pix[i+0] = bg[0]
out.Pix[i+1] = bg[1]
out.Pix[i+2] = bg[2]
out.Pix[i+3] = 0xff
}
}
if flags&hextileAnySubrects == 0 {
continue
}
n := int(body[pos])
pos++
for k := 0; k < n; k++ {
color := fg
if flags&hextileSubrectsColoured != 0 {
color = readPixel()
}
xy := body[pos]
wh := body[pos+1]
pos += 2
rxr := int(xy >> 4)
ryr := int(xy & 0x0f)
rwr := int(wh>>4) + 1
rhr := int(wh&0x0f) + 1
for ry := 0; ry < rhr; ry++ {
for rx := 0; rx < rwr; rx++ {
i := (sy+ryr+ry)*out.Stride + (sx+rxr+rx)*4
out.Pix[i+0] = color[0]
out.Pix[i+1] = color[1]
out.Pix[i+2] = color[2]
out.Pix[i+3] = 0xff
}
}
}
}
}
return out
}
func makeUniformImage(w, h int, r, g, b byte) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, w, h))
for i := 0; i < len(img.Pix); i += 4 {
img.Pix[i+0] = r
img.Pix[i+1] = g
img.Pix[i+2] = b
img.Pix[i+3] = 0xff
}
return img
}
func makeTwoColorImage(w, h int) *image.RGBA {
img := makeUniformImage(w, h, 0x10, 0x20, 0x30)
// Draw a vertical bar of fg in the middle.
fg := [3]byte{0xa0, 0xb0, 0xc0}
for y := 0; y < h; y++ {
for x := w / 4; x < w/2; x++ {
i := y*img.Stride + x*4
img.Pix[i+0] = fg[0]
img.Pix[i+1] = fg[1]
img.Pix[i+2] = fg[2]
}
}
return img
}
func compareImages(t *testing.T, want, got *image.RGBA) {
t.Helper()
if want.Rect != got.Rect {
t.Fatalf("rect mismatch: %v vs %v", want.Rect, got.Rect)
}
w, h := want.Rect.Dx(), want.Rect.Dy()
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
i := y*want.Stride + x*4
j := y*got.Stride + x*4
if want.Pix[i] != got.Pix[j] || want.Pix[i+1] != got.Pix[j+1] || want.Pix[i+2] != got.Pix[j+2] {
t.Fatalf("pixel mismatch at (%d,%d): want %v got %v",
x, y, want.Pix[i:i+3], got.Pix[j:j+3])
}
}
}
}
func TestEncodeHextileRect_Uniform(t *testing.T) {
pf := defaultClientPixelFormat()
img := makeUniformImage(64, 64, 0x33, 0x66, 0x99)
buf := encodeHextileRect(img, pf, 0, 0, 64, 64)
got := decodeHextile(t, buf, pf)
compareImages(t, img, got)
}
func TestEncodeHextileRect_TwoColor(t *testing.T) {
pf := defaultClientPixelFormat()
img := makeTwoColorImage(64, 64)
buf := encodeHextileRect(img, pf, 0, 0, 64, 64)
got := decodeHextile(t, buf, pf)
compareImages(t, img, got)
}
func TestEncodeHextileRect_Multicolor(t *testing.T) {
pf := defaultClientPixelFormat()
img := makeBenchImage(64, 64, 42)
buf := encodeHextileRect(img, pf, 0, 0, 64, 64)
got := decodeHextile(t, buf, pf)
compareImages(t, img, got)
}
func TestEncodeHextileRect_NonAligned(t *testing.T) {
pf := defaultClientPixelFormat()
img := makeTwoColorImage(50, 33) // not a multiple of 16
buf := encodeHextileRect(img, pf, 0, 0, 50, 33)
got := decodeHextile(t, buf, pf)
compareImages(t, img, got)
}

View File

@@ -330,57 +330,6 @@ func reverseBits(b byte) byte {
return r
}
// encodeZlibRect encodes a framebuffer region using Zlib compression.
// The zlib stream is continuous for the entire VNC session: noVNC creates
// one inflate context at startup and reuses it for all zlib-encoded rects.
// We must NOT reset the zlib writer between calls.
func encodeZlibRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int, z *zlibState) []byte {
bytesPerPixel := max(int(pf.bpp)/8, 1)
zw, zbuf := z.w, z.buf
// Clear the output buffer but keep the deflate dictionary intact.
zbuf.Reset()
// Encode the full rect pixel stream into the session-lived scratch buffer
// and feed zlib one row at a time. Row-granular writes amortise the per-
// Write overhead that used to dominate this function when it wrote one
// byte slice per pixel.
rowBytes := w * bytesPerPixel
total := rowBytes * h
if cap(z.scratch) < total {
z.scratch = make([]byte, total)
}
scratch := z.scratch[:total]
writePixels(scratch, img, pf, rect{x, y, w, h}, bytesPerPixel)
for row := 0; row < h; row++ {
if _, err := zw.Write(scratch[row*rowBytes : (row+1)*rowBytes]); err != nil {
log.Debugf("zlib write row %d: %v", row, err)
return nil
}
}
if err := zw.Flush(); err != nil {
log.Debugf("zlib flush: %v", err)
return nil
}
compressed := zbuf.Bytes()
// Build the FramebufferUpdate message.
buf := make([]byte, 4+12+4+len(compressed))
buf[0] = serverFramebufferUpdate
buf[1] = 0
binary.BigEndian.PutUint16(buf[2:4], 1) // 1 rectangle
binary.BigEndian.PutUint16(buf[4:6], uint16(x))
binary.BigEndian.PutUint16(buf[6:8], uint16(y))
binary.BigEndian.PutUint16(buf[8:10], uint16(w))
binary.BigEndian.PutUint16(buf[10:12], uint16(h))
binary.BigEndian.PutUint32(buf[12:16], uint32(encZlib))
binary.BigEndian.PutUint32(buf[16:20], uint32(len(compressed)))
copy(buf[20:], compressed)
return buf
}
// 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-
@@ -565,219 +514,6 @@ func encodePixel(dst []byte, pf clientPixelFormat, r, g, b byte) int {
return bytesPerPixel
}
// encodeHextileSolidRect emits a Hextile-encoded rectangle whose every pixel
// is the same color. All sub-tiles after the first inherit the background
// via a zero subencoding byte, collapsing a uniform 64×64 tile from ~16 KB
// raw (or ~1-2 KB zlib) down to ~20 bytes on the wire.
//
// The returned buffer starts with the 12-byte rect header + the hextile
// body. Callers assembling a multi-rect FramebufferUpdate append this after
// their own message header.
func encodeHextileSolidRect(r, g, b byte, pf clientPixelFormat, rc rect) []byte {
bytesPerPixel := max(int(pf.bpp)/8, 1)
// Count sub-tiles. Right/bottom sub-tiles may be smaller than 16.
cols := (rc.w + hextileSubSize - 1) / hextileSubSize
rows := (rc.h + hextileSubSize - 1) / hextileSubSize
subs := cols * rows
// Body: first sub-tile carries (subenc 0x02 + bg pixel); the rest are
// subenc 0x00 (inherit the previously-emitted background).
bodySize := 1 + bytesPerPixel + (subs - 1)
buf := make([]byte, 12+bodySize)
binary.BigEndian.PutUint16(buf[0:2], uint16(rc.x))
binary.BigEndian.PutUint16(buf[2:4], uint16(rc.y))
binary.BigEndian.PutUint16(buf[4:6], uint16(rc.w))
binary.BigEndian.PutUint16(buf[6:8], uint16(rc.h))
binary.BigEndian.PutUint32(buf[8:12], uint32(encHextile))
buf[12] = hextileBackgroundSpecified
encodePixel(buf[13:13+bytesPerPixel], pf, r, g, b)
// Remaining sub-tiles are already zero-valued from make(): "same as
// previous background", no pixel bytes.
_ = subs
return buf
}
// encodeHextileRect emits a full Hextile-encoded rectangle. Each 16×16
// sub-tile is classified as 1-color (background only), 2-color (background
// + foreground subrects), or raw. The 1-color and 2-color paths are
// significantly cheaper than zlib on UI content (text, icons, flat
// backgrounds) and avoid the persistent zlib stream's inter-rect
// serialization point, so they parallelize trivially.
//
// The returned buffer starts with the 12-byte rect header + hextile body.
func encodeHextileRect(img *image.RGBA, pf clientPixelFormat, x, y, w, h int) []byte {
bytesPerPixel := max(int(pf.bpp)/8, 1)
// Pre-size: worst case is every sub-tile raw → 1 header byte + raw
// pixels per sub-tile.
maxBody := 0
for sy := 0; sy < h; sy += hextileSubSize {
sh := min(hextileSubSize, h-sy)
for sx := 0; sx < w; sx += hextileSubSize {
sw := min(hextileSubSize, w-sx)
maxBody += 1 + sw*sh*bytesPerPixel
}
}
buf := make([]byte, 12, 12+maxBody)
binary.BigEndian.PutUint16(buf[0:2], uint16(x))
binary.BigEndian.PutUint16(buf[2:4], uint16(y))
binary.BigEndian.PutUint16(buf[4:6], uint16(w))
binary.BigEndian.PutUint16(buf[6:8], uint16(h))
binary.BigEndian.PutUint32(buf[8:12], uint32(encHextile))
var state hextileBgState
for sy := 0; sy < h; sy += hextileSubSize {
sh := min(hextileSubSize, h-sy)
for sx := 0; sx < w; sx += hextileSubSize {
sw := min(hextileSubSize, w-sx)
buf = appendHextileSubtile(buf, img, pf, rect{x + sx, y + sy, sw, sh}, &state, bytesPerPixel)
}
}
return buf
}
// hextileBgState carries the running background across sub-tile encodes so
// we can omit the BackgroundSpecified flag when it hasn't changed.
type hextileBgState struct {
prev uint32
valid bool
}
// appendHextileSubtile encodes a single 16×16 (or smaller edge) sub-tile
// onto buf.
func appendHextileSubtile(buf []byte, img *image.RGBA, pf clientPixelFormat, rc rect, state *hextileBgState, bytesPerPixel int) []byte {
x, y, w, h := rc.x, rc.y, rc.w, rc.h
c0, c1, only2, c0Count, c1Count := classifySubtile(img, x, y, w, h)
if !only2 {
// >2 distinct colours: raw fallback.
buf = append(buf, hextileRaw)
buf = appendRawPixels(buf, img, pf, rc, bytesPerPixel)
state.valid = false
return buf
}
if c1Count == 0 {
// Single colour. Background only.
if state.valid && state.prev == c0 {
return append(buf, 0)
}
buf = append(buf, hextileBackgroundSpecified)
buf = appendPackedPixelFromRGBA(buf, pf, c0, bytesPerPixel)
state.prev = c0
state.valid = true
return buf
}
// Two colours. Background = majority; foreground = minority,
// emitted as 1-row subrects of fg runs.
bg, fg := c0, c1
if c1Count > c0Count {
bg, fg = c1, c0
}
subrects := collectFgSubrects(img, x, y, w, h, bg)
// Cap at 255 (the count is a uint8). On overflow fall through to
// raw: that's the simplest correct fallback.
if len(subrects) <= 255 {
flags := byte(hextileForegroundSpecified | hextileAnySubrects)
emitBg := !state.valid || state.prev != bg
if emitBg {
flags |= hextileBackgroundSpecified
}
buf = append(buf, flags)
if emitBg {
buf = appendPackedPixelFromRGBA(buf, pf, bg, bytesPerPixel)
state.prev = bg
state.valid = true
}
buf = appendPackedPixelFromRGBA(buf, pf, fg, bytesPerPixel)
buf = append(buf, byte(len(subrects)))
for _, sr := range subrects {
buf = append(buf, byte((sr[0]<<4)|sr[1]), byte(((sr[2]-1)<<4)|(sr[3]-1)))
}
return buf
}
// Raw fallback.
buf = append(buf, hextileRaw)
buf = appendRawPixels(buf, img, pf, rc, bytesPerPixel)
// Raw sub-tiles invalidate the persistent background.
state.valid = false
return buf
}
// classifySubtile scans the sub-tile and reports up to two distinct pixel
// values plus their counts. only2 is false the moment a third distinct
// colour is seen, in which case the caller falls back to raw.
func classifySubtile(img *image.RGBA, x, y, w, h int) (c0, c1 uint32, only2 bool, c0Count, c1Count int) {
stride := img.Stride
base := y*stride + x*4
c0 = *(*uint32)(unsafe.Pointer(&img.Pix[base]))
only2 = true
for row := 0; row < h; row++ {
p := base + row*stride
for col := 0; col < w; col++ {
px := *(*uint32)(unsafe.Pointer(&img.Pix[p+col*4]))
switch {
case px == c0:
c0Count++
case c1Count == 0:
c1 = px
c1Count = 1
case px == c1:
c1Count++
default:
return c0, c1, false, 0, 0
}
}
}
return c0, c1, only2, c0Count, c1Count
}
// collectFgSubrects walks the sub-tile row by row, emitting one subrect per
// horizontal run of pixels not equal to bg. Each subrect is [subX, subY,
// width, height] with width/height in 1..16.
func collectFgSubrects(img *image.RGBA, x, y, w, h int, bg uint32) [][4]int {
stride := img.Stride
var out [][4]int
for row := 0; row < h; row++ {
p := y*stride + x*4 + row*stride
col := 0
for col < w {
if *(*uint32)(unsafe.Pointer(&img.Pix[p+col*4])) == bg {
col++
continue
}
start := col
for col < w && *(*uint32)(unsafe.Pointer(&img.Pix[p+col*4])) != bg {
col++
}
out = append(out, [4]int{start, row, col - start, 1})
}
}
return out
}
func appendPackedPixelFromRGBA(buf []byte, pf clientPixelFormat, px uint32, bytesPerPixel int) []byte {
r := byte(px)
g := byte(px >> 8)
b := byte(px >> 16)
var tmp [4]byte
encodePixel(tmp[:], pf, r, g, b)
return append(buf, tmp[:bytesPerPixel]...)
}
func appendRawPixels(buf []byte, img *image.RGBA, pf clientPixelFormat, rc rect, bytesPerPixel int) []byte {
start := len(buf)
buf = append(buf, make([]byte, rc.w*rc.h*bytesPerPixel)...)
writePixels(buf[start:], img, pf, rc, bytesPerPixel)
return buf
}
// tightState holds the per-session JPEG scratch buffer and reused encoders
// so per-rect encoding stays alloc-free in the steady state.

View File

@@ -58,16 +58,16 @@ func BenchmarkEncodeRawRect(b *testing.B) {
}
}
func BenchmarkEncodeZlibRect(b *testing.B) {
func BenchmarkEncodeTightRect(b *testing.B) {
pf := defaultClientPixelFormat()
for _, r := range benchRects {
img := makeBenchImage(r.w, r.h, 1)
z := newZlibState()
t := newTightState()
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++ {
_ = encodeZlibRect(img, pf, 0, 0, r.w, r.h, z)
_ = encodeTightRect(img, pf, 0, 0, r.w, r.h, t)
}
})
}
@@ -151,9 +151,9 @@ func BenchmarkSwizzleBGRAtoRGBANaive(b *testing.B) {
}
}
// BenchmarkEncodeUniformTile_Zlib measures the cost of sending a uniform
// 64×64 dirty tile via zlib (the old path before the Hextile fast path).
func BenchmarkEncodeUniformTile_Zlib(b *testing.B) {
// BenchmarkEncodeUniformTile_TightFill measures the fast path for a uniform
// 64×64 tile via Tight's Fill subencoding (16 wire bytes regardless of size).
func BenchmarkEncodeUniformTile_TightFill(b *testing.B) {
pf := defaultClientPixelFormat()
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
for i := 0; i < len(img.Pix); i += 4 {
@@ -162,24 +162,11 @@ func BenchmarkEncodeUniformTile_Zlib(b *testing.B) {
img.Pix[i+2] = 0x99
img.Pix[i+3] = 0xff
}
z := newZlibState()
t := newTightState()
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
out := encodeZlibRect(img, pf, 0, 0, 64, 64, z)
bytesOut = len(out)
}
b.ReportMetric(float64(bytesOut), "wire_bytes")
}
// BenchmarkEncodeUniformTile_Hextile measures the new fast path: uniform
// 64×64 tile emitted as Hextile SolidFill.
func BenchmarkEncodeUniformTile_Hextile(b *testing.B) {
pf := defaultClientPixelFormat()
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
out := encodeHextileSolidRect(0x33, 0x66, 0x99, pf, rect{0, 0, 64, 64})
out := encodeTightRect(img, pf, 0, 0, 64, 64, t)
bytesOut = len(out)
}
b.ReportMetric(float64(bytesOut), "wire_bytes")
@@ -198,7 +185,7 @@ func BenchmarkTileIsUniform(b *testing.B) {
// BenchmarkEncodeManyTilesVsFullFrame exercises the bandwidth + CPU
// trade-off that motivates the full-frame promotion path: encoding a burst
// of N dirty 64×64 tiles as separate zlib rects vs emitting one big zlib
// of N dirty 64×64 tiles as separate Tight rects vs emitting one big Tight
// rect for the whole frame.
func BenchmarkEncodeManyTilesVsFullFrame(b *testing.B) {
pf := defaultClientPixelFormat()
@@ -222,15 +209,15 @@ func BenchmarkEncodeManyTilesVsFullFrame(b *testing.B) {
}
nTiles := len(tiles)
b.Run("per_tile_zlib", func(b *testing.B) {
z := newZlibState()
b.Run("per_tile_tight", func(b *testing.B) {
t := newTightState()
b.SetBytes(int64(w * h * 4))
b.ReportAllocs()
var totalOut int
for i := 0; i < b.N; i++ {
totalOut = 0
for _, r := range tiles {
out := encodeZlibRect(img, pf, r[0], r[1], r[2], r[3], z)
out := encodeTightRect(img, pf, r[0], r[1], r[2], r[3], t)
totalOut += len(out)
}
}
@@ -238,13 +225,13 @@ func BenchmarkEncodeManyTilesVsFullFrame(b *testing.B) {
b.ReportMetric(float64(nTiles), "tiles")
})
b.Run("full_frame_zlib", func(b *testing.B) {
z := newZlibState()
b.Run("full_frame_tight", func(b *testing.B) {
t := newTightState()
b.SetBytes(int64(w * h * 4))
b.ReportAllocs()
var totalOut int
for i := 0; i < b.N; i++ {
out := encodeZlibRect(img, pf, 0, 0, w, h, z)
out := encodeTightRect(img, pf, 0, 0, w, h, t)
totalOut = len(out)
}
b.ReportMetric(float64(totalOut), "wire_bytes")
@@ -297,13 +284,13 @@ func BenchmarkEncodeCoalescedVsPerTile(b *testing.B) {
coalesced := coalesceRects(append([][4]int(nil), perTile...))
b.Run("per_tile", func(b *testing.B) {
z := newZlibState()
t := newTightState()
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
bytesOut = 0
for _, r := range perTile {
out := encodeZlibRect(img, pf, r[0], r[1], r[2], r[3], z)
out := encodeTightRect(img, pf, r[0], r[1], r[2], r[3], t)
bytesOut += len(out)
}
}
@@ -312,13 +299,13 @@ func BenchmarkEncodeCoalescedVsPerTile(b *testing.B) {
})
b.Run("coalesced", func(b *testing.B) {
z := newZlibState()
t := newTightState()
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
bytesOut = 0
for _, r := range coalesced {
out := encodeZlibRect(img, pf, r[0], r[1], r[2], r[3], z)
out := encodeTightRect(img, pf, r[0], r[1], r[2], r[3], t)
bytesOut += len(out)
}
}
@@ -352,10 +339,10 @@ func BenchmarkCoalesceRects(b *testing.B) {
}
}
// BenchmarkEncodeTightVsZlib_Photo compares Tight (which routes random/
// photographic content to JPEG) against the persistent Zlib stream. JPEG
// at quality 70 should be 5-15× smaller on this kind of content.
func BenchmarkEncodeTightVsZlib_Photo(b *testing.B) {
// BenchmarkEncodeTight_Photo measures Tight on random/photographic content.
// The internal sampledColorCount gate routes large many-colour rects to JPEG
// at quality 70.
func BenchmarkEncodeTight_Photo(b *testing.B) {
pf := defaultClientPixelFormat()
for _, r := range []struct {
name string
@@ -366,17 +353,6 @@ func BenchmarkEncodeTightVsZlib_Photo(b *testing.B) {
{"1080p", 1920, 1080},
} {
img := makeBenchImage(r.w, r.h, 1)
b.Run(r.name+"/zlib", func(b *testing.B) {
z := newZlibState()
b.SetBytes(int64(r.w * r.h * 4))
b.ReportAllocs()
var bytesOut int
for i := 0; i < b.N; i++ {
out := encodeZlibRect(img, pf, 0, 0, r.w, r.h, z)
bytesOut = len(out)
}
b.ReportMetric(float64(bytesOut), "wire_bytes")
})
b.Run(r.name+"/tight", func(b *testing.B) {
t := newTightState()
b.SetBytes(int64(r.w * r.h * 4))

View File

@@ -50,11 +50,8 @@ type session struct {
// reads them on every frame.
encMu sync.RWMutex
pf clientPixelFormat
useZlib bool
useHextile bool
useTight bool
useCopyRect bool
zlib *zlibState
tight *tightState
copyRectDet *copyRectDetector
// Pseudo-encodings the client advertised support for. Updated under
@@ -356,15 +353,6 @@ func (s *session) handleSetEncodings() error {
case pseudoEncLastRect:
s.clientSupportsLastRect = true
encs = append(encs, "last-rect")
case encZlib:
s.useZlib = true
if s.zlib == nil {
s.zlib = newZlibState()
}
encs = append(encs, "zlib")
case encHextile:
s.useHextile = true
encs = append(encs, "hextile")
case encTight:
s.useTight = true
if s.tight == nil {
@@ -705,17 +693,26 @@ func (s *session) sendFullUpdate(img *image.RGBA) error {
s.encMu.RLock()
pf := s.pf
useZlib := s.useZlib
zlib := s.zlib
useTight := s.useTight
tight := s.tight
s.encMu.RUnlock()
var buf []byte
if useZlib && zlib != nil {
buf = encodeZlibRect(img, pf, 0, 0, w, h, zlib)
} else {
buf = encodeRawRect(img, pf, 0, 0, w, h)
if useTight && tight != nil && pfIsTightCompatible(pf) {
// Tight encodes arbitrary sizes natively (Fill for uniform, JPEG
// for photo-like, Basic+zlib otherwise). Wrap the rect bytes with
// the 4-byte FramebufferUpdate header.
rectBuf := encodeTightRect(img, pf, 0, 0, w, h, tight)
buf := make([]byte, 4+len(rectBuf))
buf[0] = serverFramebufferUpdate
binary.BigEndian.PutUint16(buf[2:4], 1)
copy(buf[4:], rectBuf)
s.writeMu.Lock()
_, err := s.conn.Write(buf)
s.writeMu.Unlock()
return err
}
buf := encodeRawRect(img, pf, 0, 0, w, h)
s.writeMu.Lock()
_, err := s.conn.Write(buf)
s.writeMu.Unlock()
@@ -761,46 +758,25 @@ func (s *session) sendDirtyAndMoves(img *image.RGBA, moves []copyRectMove, rects
return nil
}
// encodeTile produces the on-wire rect bytes for a single dirty tile,
// picking the cheapest encoding available:
// - Hextile SolidFill when the tile is a single colour (~20 bytes for a
// 64×64 tile instead of ~1-2 KB zlib, ~16 KB raw).
// - Zlib when the client negotiated it.
// - Raw otherwise.
// encodeTile produces the on-wire rect bytes for a single dirty tile. Tight
// is the only non-Raw encoding we negotiate: uniform tiles collapse to its
// Fill subencoding (~16 bytes), photo-like rects route to JPEG, and the
// rest take the Basic+zlib path. Raw is the fallback when Tight is not
// negotiated or the negotiated pixel format is incompatible with Tight's
// mandatory 24-bit RGB TPIXEL encoding.
//
// Output omits the 4-byte FramebufferUpdate header; callers combine multiple
// tiles into one message.
func (s *session) encodeTile(img *image.RGBA, x, y, w, h int) []byte {
s.encMu.RLock()
pf := s.pf
useHextile := s.useHextile
useTight := s.useTight
tight := s.tight
useZlib := s.useZlib
zlib := s.zlib
s.encMu.RUnlock()
if useHextile {
if pixel, uniform := tileIsUniform(img, x, y, w, h); uniform {
r := byte(pixel)
g := byte(pixel >> 8)
b := byte(pixel >> 16)
return encodeHextileSolidRect(r, g, b, pf, rect{x, y, w, h})
}
// Full Hextile encoder disabled pending investigation of 16x16
// red-tile artifacts on Windows. Solid-fill fast path is safe.
}
// Larger merged rects: prefer Tight (JPEG for photo-like, Basic+zlib
// otherwise) when the client supports it AND the negotiated format is
// compatible with Tight's mandatory 24-bit RGB TPIXEL encoding. Tight is
// dramatically better than RFB Zlib on photographic content and
// competitive on UI.
if useTight && tight != nil && pfIsTightCompatible(pf) {
return encodeTightRect(img, pf, x, y, w, h, tight)
}
if useZlib && zlib != nil {
return encodeZlibRect(img, pf, x, y, w, h, zlib)[4:]
}
return encodeRawRect(img, pf, x, y, w, h)[4:]
}

View File

@@ -2,10 +2,36 @@ package server
import (
"bytes"
"image"
"image/jpeg"
"testing"
)
func makeUniformImage(w, h int, r, g, b byte) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, w, h))
for i := 0; i < len(img.Pix); i += 4 {
img.Pix[i+0] = r
img.Pix[i+1] = g
img.Pix[i+2] = b
img.Pix[i+3] = 0xff
}
return img
}
func makeTwoColorImage(w, h int) *image.RGBA {
img := makeUniformImage(w, h, 0x10, 0x20, 0x30)
fg := [3]byte{0xa0, 0xb0, 0xc0}
for y := 0; y < h; y++ {
for x := w / 4; x < w/2; x++ {
i := y*img.Stride + x*4
img.Pix[i+0] = fg[0]
img.Pix[i+1] = fg[1]
img.Pix[i+2] = fg[2]
}
}
return img
}
func decodeTightLength(buf []byte) (n, consumed int) {
b0 := buf[0]
n = int(b0 & 0x7f)