diff --git a/client/vnc/server/hextile_test.go b/client/vnc/server/hextile_test.go deleted file mode 100644 index 69a1904d4..000000000 --- a/client/vnc/server/hextile_test.go +++ /dev/null @@ -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) -} diff --git a/client/vnc/server/rfb.go b/client/vnc/server/rfb.go index f9a3af485..6dcb57a96 100644 --- a/client/vnc/server/rfb.go +++ b/client/vnc/server/rfb.go @@ -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. diff --git a/client/vnc/server/rfb_bench_test.go b/client/vnc/server/rfb_bench_test.go index c26672da9..4a0115536 100644 --- a/client/vnc/server/rfb_bench_test.go +++ b/client/vnc/server/rfb_bench_test.go @@ -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)) diff --git a/client/vnc/server/session.go b/client/vnc/server/session.go index f80fae600..7433731c7 100644 --- a/client/vnc/server/session.go +++ b/client/vnc/server/session.go @@ -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:] } diff --git a/client/vnc/server/tight_test.go b/client/vnc/server/tight_test.go index 0e0aaab4d..698f703e8 100644 --- a/client/vnc/server/tight_test.go +++ b/client/vnc/server/tight_test.go @@ -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)