mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-19 15:19:55 +00:00
192 lines
6.5 KiB
Go
192 lines
6.5 KiB
Go
//go:build !js && !ios && !android
|
|
|
|
package server
|
|
|
|
import (
|
|
"hash/maphash"
|
|
"image"
|
|
)
|
|
|
|
// copyRectDetector finds tiles in the current frame that match the content
|
|
// of some tile-aligned region of the previous frame, so we can emit them as
|
|
// CopyRect rectangles (16 wire bytes) instead of re-encoding the pixels.
|
|
//
|
|
// The detector keeps two structures:
|
|
// - tileHash, a flat slice of one hash per tile-aligned position, used as
|
|
// the source of truth for the previous frame's tile content.
|
|
// - prevTiles, a hash → position lookup used during findTileMatch.
|
|
//
|
|
// updateDirty rehashes only the tiles that changed this frame, so the
|
|
// steady-state cost is proportional to the dirty set, not the framebuffer.
|
|
// A full rebuild from scratch is only done on the first frame or when the
|
|
// detector has not yet been initialized for the current resolution.
|
|
//
|
|
// Limitations:
|
|
// - Only tile-aligned source positions are considered. Sub-tile-aligned
|
|
// moves (e.g. window dragged by 7 pixels) are not detected. This still
|
|
// covers the common case of vertical/horizontal scrolling, which always
|
|
// produces tile-aligned matches at the tile granularity.
|
|
// - 64-bit maphash collisions are assumed not to happen. The probability
|
|
// for any single frame's hash universe is ~2^-32 * tileCount² which is
|
|
// vanishingly small at typical resolutions; if we ever observe one we
|
|
// can fall back to a full memcmp verification.
|
|
type copyRectDetector struct {
|
|
seed maphash.Seed
|
|
tileSize int
|
|
w, h int
|
|
cols, rows int
|
|
// tileHash[ty*cols + tx] is the current hash of the tile at (tx, ty)
|
|
// in the previous frame. Lookup uses this to detect stale prevTiles
|
|
// entries — incremental updates may leave hash→pos entries pointing
|
|
// at a tile whose content has since changed.
|
|
tileHash []uint64
|
|
// prevTiles maps a tile hash to a (x, y) origin in the previous frame.
|
|
prevTiles map[uint64][2]int
|
|
// hash is reused across hash computations to keep the per-tile lookup
|
|
// path allocation-free.
|
|
hash maphash.Hash
|
|
}
|
|
|
|
func newCopyRectDetector(tileSize int) *copyRectDetector {
|
|
d := ©RectDetector{
|
|
seed: maphash.MakeSeed(),
|
|
tileSize: tileSize,
|
|
prevTiles: make(map[uint64][2]int),
|
|
}
|
|
d.hash.SetSeed(d.seed)
|
|
return d
|
|
}
|
|
|
|
// resize ensures the per-tile tables match the given framebuffer size.
|
|
// Called from rebuild before each full hash sweep.
|
|
func (d *copyRectDetector) resize(w, h int) {
|
|
if d.w == w && d.h == h && d.tileHash != nil {
|
|
return
|
|
}
|
|
d.w, d.h = w, h
|
|
d.cols = w / d.tileSize
|
|
d.rows = h / d.tileSize
|
|
d.tileHash = make([]uint64, d.cols*d.rows)
|
|
}
|
|
|
|
// hashTile computes the 64-bit maphash of one tile-aligned tile of frame.
|
|
func (d *copyRectDetector) hashTile(frame *image.RGBA, tx, ty int) uint64 {
|
|
d.hash.Reset()
|
|
ts := d.tileSize
|
|
stride := frame.Stride
|
|
rowBytes := ts * 4
|
|
base := ty*stride + tx*4
|
|
for row := 0; row < ts; row++ {
|
|
off := base + row*stride
|
|
_, _ = d.hash.Write(frame.Pix[off : off+rowBytes])
|
|
}
|
|
return d.hash.Sum64()
|
|
}
|
|
|
|
// rebuild discards everything and rehashes the whole frame. O(w*h). Use
|
|
// for the first frame or after the detector has been resized. Steady-state
|
|
// updates should go through updateDirty instead.
|
|
func (d *copyRectDetector) rebuild(frame *image.RGBA, w, h int) {
|
|
d.resize(w, h)
|
|
if d.prevTiles == nil {
|
|
d.prevTiles = make(map[uint64][2]int)
|
|
} else {
|
|
clear(d.prevTiles)
|
|
}
|
|
ts := d.tileSize
|
|
for ty := 0; ty+ts <= h; ty += ts {
|
|
for tx := 0; tx+ts <= w; tx += ts {
|
|
sum := d.hashTile(frame, tx, ty)
|
|
d.tileHash[(ty/ts)*d.cols+(tx/ts)] = sum
|
|
if _, exists := d.prevTiles[sum]; !exists {
|
|
d.prevTiles[sum] = [2]int{tx, ty}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateDirty rehashes only the tiles named in dirty (each entry is
|
|
// [x, y, w, h] with w and h equal to tileSize). O(len(dirty)) work, which
|
|
// in the common case is a tiny fraction of the whole framebuffer.
|
|
//
|
|
// The prevTiles map is replaced on collision rather than first-wins so a
|
|
// newly-hashed tile claims the slot. Old, stale entries pointing at tiles
|
|
// that no longer carry that hash are filtered at lookup time via tileHash.
|
|
func (d *copyRectDetector) updateDirty(frame *image.RGBA, w, h int, dirty [][4]int) {
|
|
if d.w != w || d.h != h || d.tileHash == nil {
|
|
d.rebuild(frame, w, h)
|
|
return
|
|
}
|
|
ts := d.tileSize
|
|
for _, r := range dirty {
|
|
if r[2] != ts || r[3] != ts {
|
|
continue
|
|
}
|
|
tx, ty := r[0], r[1]
|
|
if tx+ts > w || ty+ts > h {
|
|
continue
|
|
}
|
|
sum := d.hashTile(frame, tx, ty)
|
|
d.tileHash[(ty/ts)*d.cols+(tx/ts)] = sum
|
|
// Latest-wins on collision: ensures the most recent owner of this
|
|
// hash is the one we'll return on lookup. The previous owner's
|
|
// entry, if any, gets shadowed; if its content has changed it's
|
|
// stale anyway and findTileMatch's verification will skip it.
|
|
d.prevTiles[sum] = [2]int{tx, ty}
|
|
}
|
|
}
|
|
|
|
// findTileMatch hashes the current-frame tile at (dstX, dstY) and looks up
|
|
// its hash in the previous-frame map. Returns (srcX, srcY, true) when a
|
|
// matching tile-aligned tile exists at a different position whose stored
|
|
// hash still equals the requested hash (so the result is not stale).
|
|
func (d *copyRectDetector) findTileMatch(cur *image.RGBA, dstX, dstY int) (int, int, bool) {
|
|
if len(d.prevTiles) == 0 || d.tileHash == nil {
|
|
return 0, 0, false
|
|
}
|
|
ts := d.tileSize
|
|
if dstX+ts > cur.Rect.Dx() || dstY+ts > cur.Rect.Dy() {
|
|
return 0, 0, false
|
|
}
|
|
sum := d.hashTile(cur, dstX, dstY)
|
|
pos, ok := d.prevTiles[sum]
|
|
if !ok {
|
|
return 0, 0, false
|
|
}
|
|
if pos[0] == dstX && pos[1] == dstY {
|
|
return 0, 0, false
|
|
}
|
|
// Reject stale entries: the position the map points at must still
|
|
// carry the same hash according to our per-tile array.
|
|
if d.tileHash[(pos[1]/ts)*d.cols+(pos[0]/ts)] != sum {
|
|
return 0, 0, false
|
|
}
|
|
return pos[0], pos[1], true
|
|
}
|
|
|
|
// extractCopyRectTiles examines the diff-produced (per-tile) dirty list and
|
|
// pulls out any tiles whose current-frame content matches a prev-frame tile
|
|
// at a different position. Returns the CopyRect candidates and the residual
|
|
// dirty tiles that still need pixel encoding.
|
|
type copyRectMove struct {
|
|
srcX, srcY int
|
|
dstX, dstY int
|
|
}
|
|
|
|
func (d *copyRectDetector) extractCopyRectTiles(cur *image.RGBA, dirtyTiles [][4]int) (moves []copyRectMove, remaining [][4]int) {
|
|
ts := d.tileSize
|
|
remaining = dirtyTiles[:0:cap(dirtyTiles)]
|
|
for _, r := range dirtyTiles {
|
|
if r[2] == ts && r[3] == ts {
|
|
if sx, sy, ok := d.findTileMatch(cur, r[0], r[1]); ok {
|
|
moves = append(moves, copyRectMove{
|
|
srcX: sx, srcY: sy, dstX: r[0], dstY: r[1],
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
remaining = append(remaining, r)
|
|
}
|
|
return moves, remaining
|
|
}
|