mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 07:09:56 +00:00
Drop dead Hextile and standalone Zlib encoding paths
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:]
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user