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

@@ -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.