Files

711 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// clear.go 实现 MS-RDPEGFX 2.2.4 ClearCodec(TS_CLEARCODEC_BITMAP_STREAM)
// 的完整解码,算法对齐 FreeRDP libfreerdp/codec/clear.c。
//
// 流结构:glyphFlags(1) + seqNumber(1) + [glyph 段] + residualByteCount(4) +
// bandsByteCount(4) + subcodecByteCount(4) + 三段载荷。
package rdpgfx
import (
"encoding/binary"
"log/slog"
)
const (
clearFlagGlyphIndex = 0x01
clearFlagGlyphHit = 0x02
clearFlagCacheReset = 0x04
clearVBarSize = 32768
clearShortVBarSize = 16384
clearGlyphSize = 4000
)
// nsCodecDisabled 诊断开关:置 true 时丢弃 NSCodec 矩形
var nsCodecDisabled = false
type clearCodecCtx struct {
vBarStorage [clearVBarSize]vBarEntry
shortVBarStorage [clearShortVBarSize]vBarEntry
vBarCursor int
shortVBarCursor int
glyphCache [clearGlyphSize][]byte
seqNumber uint32
logged [24]bool
}
// logOnce 每类失败只打第一条日志,避免刷屏;位序号定位失败类别
func (ctx *clearCodecCtx) logOnce(slot int, msg string, args ...any) {
if ctx.logged[slot] {
return
}
ctx.logged[slot] = true
slog.Warn("clear:"+msg, args...)
}
func newClearCodecCtx() *clearCodecCtx {
return &clearCodecCtx{}
}
// decode 解出一张 w×h 的 BGRA 位图;流非法时返回 nil——调用方必须保留
// surface 原内容(对齐 FreeRDP 的整帧拒绝语义)。服务器会发送子编码矩形
// 完全越界的"空更新"流,若不拒绝会把清零缓冲 blit 到屏幕形成花屏块。
func (ctx *clearCodecCtx) decode(data []byte, w, h int) []byte {
out := make([]byte, w*h*4)
if len(data) < 2 {
return nil
}
off := 0
glyphFlags := data[off]
seqNumber := data[off+1]
off += 2
// 序列号失步(丢包/上下文重置)时重新同步而不是丢弃整帧
if ctx.seqNumber != uint32(seqNumber) {
ctx.seqNumber = uint32(seqNumber)
}
ctx.seqNumber = (ctx.seqNumber + 1) % 256
if glyphFlags&clearFlagCacheReset != 0 {
// FreeRDP 语义:重置游标并重新分配存储 → 所有缓存条目失效
for i := range ctx.vBarStorage {
ctx.vBarStorage[i] = vBarEntry{}
}
for i := range ctx.shortVBarStorage {
ctx.shortVBarStorage[i] = vBarEntry{}
}
ctx.vBarCursor = 0
ctx.shortVBarCursor = 0
}
// MS-RDPEGFX/FreeRDP 语义:GLYPH_HIT 必须与 GLYPH_INDEX 同置(0x03=命中),
// 单独的 HIT 是非法组合
glyphIdx := -1
if glyphFlags&clearFlagGlyphHit != 0 {
if glyphFlags&clearFlagGlyphIndex == 0 {
return nil
}
if off+2 > len(data) {
return nil
}
idx := int(binary.LittleEndian.Uint16(data[off:]))
off += 2
if idx >= clearGlyphSize {
return nil
}
if cached := ctx.glyphCache[idx]; len(cached) == len(out) {
copy(out, cached)
}
return out
}
if glyphFlags&clearFlagGlyphIndex != 0 {
if off+2 > len(data) {
return nil
}
glyphIdx = int(binary.LittleEndian.Uint16(data[off:]))
off += 2
if glyphIdx >= clearGlyphSize {
return nil
}
}
if off+12 > len(data) {
// FreeRDP:GLYPH_HIT|INDEX 的纯命中流允许没有 12 字节长度头
return nil
}
residualLen := int(binary.LittleEndian.Uint32(data[off:]))
bandsLen := int(binary.LittleEndian.Uint32(data[off+4:]))
subcodecLen := int(binary.LittleEndian.Uint32(data[off+8:]))
off += 12
if residualLen > 0 {
if off+residualLen > len(data) {
return nil
}
if !ctx.clearDecodeResidual(data[off:off+residualLen], w, h, out) {
return nil
}
}
off += residualLen
if bandsLen > 0 {
if off+bandsLen > len(data) {
return nil
}
if !ctx.clearDecodeBands(data[off:off+bandsLen], w, h, out) {
return nil
}
}
off += bandsLen
if subcodecLen > 0 {
if off+subcodecLen > len(data) {
return nil
}
if !ctx.clearDecodeSubcodecs(data[off:off+subcodecLen], w, h, out) {
return nil
}
}
if glyphIdx >= 0 {
cached := make([]byte, len(out))
copy(cached, out)
ctx.glyphCache[glyphIdx] = cached
}
return out
}
// clearDecodeResidual:(b,g,r,runLen) 游程填充,runLen 用 1/2/4 字节变长编码。
// 返回 false 表示流非法(对齐 FreeRDP 的整帧拒绝语义)。
func (ctx *clearCodecCtx) clearDecodeResidual(data []byte, w, h int, out []byte) bool {
off, pixelIndex, pixelCount := 0, 0, w*h
for off < len(data) {
if off+4 > len(data) {
ctx.logOnce(2, "residual truncated (entry hdr)")
return false
}
b, g, r := data[off], data[off+1], data[off+2]
run := int(data[off+3])
off += 4
if run >= 0xFF {
if off+2 > len(data) {
ctx.logOnce(2, "residual truncated (run16 hdr)")
return false
}
run = int(binary.LittleEndian.Uint16(data[off:]))
off += 2
if run >= 0xFFFF {
if off+4 > len(data) {
ctx.logOnce(2, "residual truncated (run32 hdr)")
return false
}
run = int(binary.LittleEndian.Uint32(data[off:]))
off += 4
}
}
if run > pixelCount-pixelIndex {
ctx.logOnce(3, "residual run overflow", "run", run, "left", pixelCount-pixelIndex)
return false
}
i := pixelIndex * 4
for n := 0; n < run; n++ {
out[i], out[i+1], out[i+2], out[i+3] = b, g, r, 0xFF
i += 4
}
pixelIndex += run
}
if pixelIndex != pixelCount {
// FreeRDP:residual 必须恰好铺满整幅,否则整帧失败
ctx.logOnce(16, "residual incomplete", "covered", pixelIndex, "want", pixelCount)
return false
}
return true
}
// clearDecodeBands:vBar 列带解码(含短 vBar 缓存/整 vBar 缓存/背景合成)。
// 返回 false 表示流非法(整帧拒绝)。
func (ctx *clearCodecCtx) clearDecodeBands(data []byte, w, h int, out []byte) bool {
off := 0
for off+11 <= len(data) {
xStart := int(binary.LittleEndian.Uint16(data[off:]))
xEnd := int(binary.LittleEndian.Uint16(data[off+2:]))
yStart := int(binary.LittleEndian.Uint16(data[off+4:]))
yEnd := int(binary.LittleEndian.Uint16(data[off+6:]))
cb, cg, cr := data[off+8], data[off+9], data[off+10]
off += 11
if xEnd < xStart || yEnd < yStart {
ctx.logOnce(4, "bands bad rect", "xStart", xStart, "xEnd", xEnd, "yStart", yStart, "yEnd", yEnd)
return false
}
colorBkg := [4]byte{cb, cg, cr, 0xFF}
vBarCount := xEnd - xStart + 1
vBarHeight := yEnd - yStart + 1
if vBarHeight > 52 {
ctx.logOnce(5, "bands vBarHeight>52", "h", vBarHeight)
return false
}
for i := 0; i < vBarCount; i++ {
if off+2 > len(data) {
return false
}
vBarHeader := binary.LittleEndian.Uint16(data[off:])
off += 2
var entry *vBarEntry
var shortEntry *vBarEntry
vBarUpdate := false
vBarYOn := 0
vBarShortPixelCount := 0
switch {
case vBarHeader&0xC000 == 0x4000: // SHORT_VBAR_CACHE_HIT
idx := int(vBarHeader & 0x3FFF)
if idx >= clearShortVBarSize {
ctx.logOnce(6, "bands short idx range", "idx", idx)
return false
}
shortEntry = &ctx.shortVBarStorage[idx]
if off >= len(data) {
return false
}
vBarYOn = int(data[off])
off++
vBarShortPixelCount = shortEntry.count
vBarUpdate = true
case vBarHeader&0xC000 == 0x0000: // SHORT_VBAR_CACHE_MISS
vBarYOn = int(vBarHeader & 0xFF)
vBarYOff := int((vBarHeader >> 8) & 0x3F)
if vBarYOff < vBarYOn {
ctx.logOnce(7, "bands yOff<yOn", "yOff", vBarYOff, "yOn", vBarYOn)
return false
}
vBarShortPixelCount = vBarYOff - vBarYOn
if vBarShortPixelCount > 52 {
ctx.logOnce(8, "bands short count>52", "n", vBarShortPixelCount)
return false
}
if off+vBarShortPixelCount*3 > len(data) {
return false
}
shortEntry = &ctx.shortVBarStorage[ctx.shortVBarCursor]
shortEntry.count = vBarShortPixelCount
shortEntry.pixels = make([]byte, vBarShortPixelCount*4)
for p := 0; p < vBarShortPixelCount; p++ {
shortEntry.pixels[p*4] = data[off+p*3]
shortEntry.pixels[p*4+1] = data[off+p*3+1]
shortEntry.pixels[p*4+2] = data[off+p*3+2]
shortEntry.pixels[p*4+3] = 0xFF
}
off += vBarShortPixelCount * 3
ctx.shortVBarCursor = (ctx.shortVBarCursor + 1) % clearShortVBarSize
vBarUpdate = true
case vBarHeader&0x8000 == 0x8000: // VBAR_CACHE_HIT
idx := int(vBarHeader & 0x7FFF)
if idx >= clearVBarSize {
ctx.logOnce(9, "bands vbar idx range", "idx", idx)
return false
}
entry = &ctx.vBarStorage[idx]
if entry.pixels == nil {
// 缓存被重置后的命中:填充哑数据
entry.count = vBarHeight
entry.pixels = make([]byte, vBarHeight*4)
}
default:
ctx.logOnce(10, "bands invalid vBarHeader", "hdr", vBarHeader)
return false
}
if vBarUpdate {
ve := &ctx.vBarStorage[ctx.vBarCursor]
ve.count = vBarHeight
ve.pixels = make([]byte, vBarHeight*4)
// 前段背景 [0, vBarYOn)
bgFront := vBarYOn
if bgFront > vBarHeight {
bgFront = vBarHeight
}
for y2 := 0; y2 < bgFront; y2++ {
copy(ve.pixels[y2*4:(y2+1)*4], colorBkg[:])
}
// 中段:短 vBar 像素 [vBarYOn, vBarYOn+vBarShortPixelCount)
count := vBarShortPixelCount
if vBarYOn+count > vBarHeight {
count = vBarHeight - vBarYOn
}
if count < 0 {
count = 0
}
if count > 0 && shortEntry != nil {
copy(ve.pixels[vBarYOn*4:(vBarYOn+count)*4], shortEntry.pixels[0:count*4])
}
// 后段背景 [vBarYOn+vBarShortPixelCount, vBarHeight)
start := vBarYOn + vBarShortPixelCount
if start < 0 {
start = 0
}
if start > vBarHeight {
start = vBarHeight
}
for y2 := start; y2 < vBarHeight; y2++ {
copy(ve.pixels[y2*4:(y2+1)*4], colorBkg[:])
}
ctx.vBarCursor = (ctx.vBarCursor + 1) % clearVBarSize
entry = ve
}
if entry == nil || entry.count != vBarHeight {
continue
}
// 落到输出:第 i 列,从 yStart 起取 min(count, h-yStart) 像素
x := xStart + i
if x >= w {
continue
}
count := entry.count
if count > h-yStart {
count = h - yStart
}
if count <= 0 {
continue
}
for y := 0; y < count; y++ {
dst := (yStart+y)*w*4 + x*4
copy(out[dst:dst+4], entry.pixels[y*4:y*4+4])
}
}
}
return true
}
// clearDecodeSubcodecs:矩形级子编码(0=BGR24 原始,1=NSCodec,2=RLEX)。
// 返回 false 表示流非法(整帧拒绝)。
func (ctx *clearCodecCtx) clearDecodeSubcodecs(data []byte, w, h int, out []byte) bool {
off := 0
for off+13 <= len(data) {
xStart := int(binary.LittleEndian.Uint16(data[off:]))
yStart := int(binary.LittleEndian.Uint16(data[off+2:]))
width := int(binary.LittleEndian.Uint16(data[off+4:]))
height := int(binary.LittleEndian.Uint16(data[off+6:]))
bmpLen := int(binary.LittleEndian.Uint32(data[off+8:]))
subcodecId := data[off+12]
off += 13
if off+bmpLen > len(data) {
ctx.logOnce(11, "subcodec payload truncated", "id", subcodecId, "want", bmpLen, "have", len(data)-off)
return false
}
payload := data[off : off+bmpLen]
off += bmpLen
// FreeRDP 语义:子编码矩形越界是非法流,整帧拒绝(否则服务器发来的
// 完全越界"空更新"矩形会把清零缓冲 blit 到屏幕形成花屏块)
if xStart >= w || yStart >= h || xStart+width > w || yStart+height > h {
ctx.logOnce(17, "subcodec rect out of bounds",
"id", subcodecId, "x", xStart, "y", yStart, "rw", width, "rh", height, "w", w, "h", h)
return false
}
switch subcodecId {
case 0: // 未压缩 BGR24
if len(payload) != width*height*3 {
ctx.logOnce(18, "subcodec BGR24 size mismatch", "want", width*height*3, "have", len(payload))
return false
}
clearWriteBGR24(payload, width, height, out, xStart, yStart, w, h)
case 1: // NSCodec(YCoCg 色域压缩编码)
if !clearDecodeNSCodec(payload, width, height, out, xStart, yStart, w, h) {
return false
}
case 2: // RLEX
if !ctx.clearDecodeRLEX(payload, width, height, out, xStart, yStart, w, h) {
return false
}
default:
ctx.logOnce(15, "unknown subcodec id", "id", subcodecId)
return false
}
}
return true
}
func clearWriteBGR24(data []byte, w, h int, out []byte, xDst, yDst, surfW, surfH int) {
stride := w * 3
if stride*h > len(data) {
return
}
for y := 0; y < h; y++ {
if yDst+y >= surfH {
return
}
row := data[y*stride : (y+1)*stride]
for x := 0; x < w; x++ {
if xDst+x >= surfW {
break
}
dst := (yDst+y)*surfW*4 + (xDst+x)*4
out[dst], out[dst+1], out[dst+2], out[dst+3] = row[x*3], row[x*3+1], row[x*3+2], 0xFF
}
}
}
// clearDecodeNSCodec 解码 NSCodec 子编码矩形,算法对齐 FreeRDP libfreerdp/codec/nsc.c:
// 20 字节头(4×PlaneByteCount + ColorLossLevel + ChromaSubsamplingLevel + 保留 2 字节),
// 4 个色度平面(Y/Co/Cg/A)RLE 解压后做色损恢复与 YCoCg→RGB 转换。
func clearDecodeNSCodec(data []byte, w, h int, out []byte, xDst, yDst, surfW, surfH int) bool {
if len(data) < 20 {
return false
}
var planeCount [4]int
total := 0
for i := 0; i < 4; i++ {
planeCount[i] = int(binary.LittleEndian.Uint32(data[i*4:]))
total += planeCount[i]
}
colorLossLevel := int(data[16])
chroma := int(data[17])
if colorLossLevel < 1 || colorLossLevel > 7 {
return false
}
planes := data[20:]
if len(planes) < total {
return false
}
rw := (w + 7) &^ 7
rh := (h + 1) &^ 1
// 原始平面字节数(OrgByteCount)
org := [4]int{w * h, w * h, w * h, w * h}
if chroma != 0 {
org[0] = rw * h
org[1] = (rw / 2) * (rh / 2)
org[2] = org[1]
}
var pbuf [4][]byte
for i := 0; i < 4; i++ {
pbuf[i] = make([]byte, org[i])
}
off := 0
for i := 0; i < 4; i++ {
psize := planeCount[i]
plane := planes[off : off+psize]
off += psize
switch {
case psize == 0:
for j := range pbuf[i] {
pbuf[i][j] = 0xFF
}
case psize < org[i]:
if !nscRLEDecode(plane, pbuf[i]) {
return false
}
default:
copy(pbuf[i], plane[:org[i]])
}
}
// 色损恢复 + YCoCg→RGB:shift = ColorLossLevel-1
shift := uint(colorLossLevel - 1)
bmp := make([]byte, w*h*4)
pos := 0
for y := 0; y < h; y++ {
var yplane, coplane, cgplane []byte
if chroma != 0 {
yplane = pbuf[0][y*rw:]
coplane = pbuf[1][(y>>1)*(rw>>1):]
cgplane = pbuf[2][(y>>1)*(rw>>1):]
} else {
yplane = pbuf[0][y*w:]
coplane = pbuf[1][y*w:]
cgplane = pbuf[2][y*w:]
}
// A 平面(pbuf[3])不参与显示:canvas putImageData 会保留 alpha,
// 非 255 的 alpha 会呈现半透明色块,故强制输出不透明
coIdx, cgIdx := 0, 0
for x := 0; x < w; x++ {
yv := int16(yplane[x])
// C: (INT16)(INT8)(((INT16)u8) << shift) —— 16 位环绕后截断低 8 位再符号扩展
cov := int16(int8(byte(int16(coplane[coIdx]) << shift)))
cgv := int16(int8(byte(int16(cgplane[cgIdx]) << shift)))
rv := yv + cov - cgv
gv := yv + cgv
bv := yv - cov - cgv
bmp[pos] = nscClamp(bv)
bmp[pos+1] = nscClamp(gv)
bmp[pos+2] = nscClamp(rv)
// A 平面值不作为透明度使用:RDP 桌面内容恒不透明,而 canvas
// putImageData 会保留 alpha,若 A 平面含非 255 值会呈现半透明色块
bmp[pos+3] = 0xFF
pos += 4
if chroma != 0 {
if x%2 == 1 {
coIdx++
cgIdx++
}
} else {
coIdx++
cgIdx++
}
}
}
blitBGRA(bmp, w, h, out, xDst, yDst, surfW, surfH)
return true
}
// nscRLEDecode 单平面 NSC 游程解码,对齐 FreeRDP nsc_rle_decode
func nscRLEDecode(in, out []byte) bool {
inOff, outOff := 0, 0
left := len(out)
for left > 4 {
if inOff >= len(in) {
return false
}
value := in[inOff]
inOff++
if left == 5 {
out[outOff] = value
outOff++
left--
} else if inOff >= len(in) {
return false
} else if value == in[inOff] {
inOff++
if inOff >= len(in) {
return false
}
var run int
if in[inOff] < 0xFF {
run = int(in[inOff]) + 2
inOff++
} else {
if inOff+5 > len(in) {
return false
}
inOff++
run = int(in[inOff]) | int(in[inOff+1])<<8 | int(in[inOff+2])<<16 | int(in[inOff+3])<<24
inOff += 4
}
if run > len(out)-outOff || run > left {
return false
}
for j := 0; j < run; j++ {
out[outOff+j] = value
}
outOff += run
left -= run
} else {
out[outOff] = value
outOff++
left--
}
}
if len(out)-outOff < 4 || left < 4 || len(in)-inOff < 4 {
return false
}
copy(out[outOff:outOff+4], in[inOff:inOff+4])
return true
}
func nscClamp(v int16) byte {
if v < 0 {
return 0
}
if v > 255 {
return 255
}
return byte(v)
}
func blitBGRA(src []byte, w, h int, out []byte, xDst, yDst, surfW, surfH int) {
for y := 0; y < h; y++ {
dy := yDst + y
if dy >= surfH {
return
}
for x := 0; x < w; x++ {
dx := xDst + x
if dx >= surfW {
break
}
copy(out[(dy*surfW+dx)*4:(dy*surfW+dx)*4+4], src[(y*w+x)*4:(y*w+x)*4+4])
}
}
}
// clearDecodeRLEX:调色板游程编码(套位打包索引)
func (ctx *clearCodecCtx) clearDecodeRLEX(data []byte, w, h int, out []byte, xDst, yDst, surfW, surfH int) bool {
if len(data) < 1 {
return false
}
paletteCount := int(data[0])
if paletteCount < 1 || paletteCount > 127 {
return false
}
if 1+paletteCount*3 > len(data) {
return false
}
palette := make([][4]byte, paletteCount)
off := 1
for i := 0; i < paletteCount; i++ {
palette[i] = [4]byte{data[off], data[off+1], data[off+2], 0xFF} // b,g,r
off += 3
}
numBits := clearLog2Floor(paletteCount-1) + 1
pixelCount := w * h
pixelIndex := 0
x, y := 0, 0
putPixel := func(c [4]byte) {
if xDst+x < surfW && yDst+y < surfH {
dst := (yDst+y)*surfW*4 + (xDst+x)*4
copy(out[dst:dst+4], c[:])
}
if x++; x >= w {
y++
x = 0
}
}
for off+2 <= len(data) && pixelIndex < pixelCount {
tmp := data[off]
run := int(data[off+1])
off += 2
suiteDepth := int(tmp >> uint(numBits) & clear8BitMask(8-numBits))
stopIndex := int(tmp & clear8BitMask(numBits))
startIndex := stopIndex - suiteDepth
if run >= 0xFF {
if off+2 > len(data) {
ctx.logOnce(13, "rlex truncated (run16 hdr)")
return false
}
run = int(binary.LittleEndian.Uint16(data[off:]))
off += 2
if run >= 0xFFFF {
if off+4 > len(data) {
ctx.logOnce(13, "rlex truncated (run32 hdr)")
return false
}
run = int(binary.LittleEndian.Uint32(data[off:]))
off += 4
}
}
if startIndex < 0 || startIndex >= paletteCount || stopIndex >= paletteCount {
ctx.logOnce(12, "rlex bad palette index", "start", startIndex, "stop", stopIndex, "count", paletteCount)
return false
}
if run > pixelCount-pixelIndex {
ctx.logOnce(14, "rlex run overflow", "run", run, "left", pixelCount-pixelIndex)
return false
}
for i := 0; i < run; i++ {
putPixel(palette[startIndex])
}
pixelIndex += run
for i := 0; i <= suiteDepth && pixelIndex < pixelCount; i++ {
putPixel(palette[startIndex+i])
pixelIndex++
}
}
if pixelIndex != pixelCount {
// FreeRDP:RLEX 必须恰好覆盖矩形,否则整帧失败
ctx.logOnce(19, "rlex incomplete", "covered", pixelIndex, "want", pixelCount)
return false
}
return true
}
func clear8BitMask(bits int) byte {
if bits <= 0 || bits > 8 {
return 0
}
return byte((1 << uint(bits)) - 1)
}
func clearLog2Floor(v int) int {
log := 0
for v > 1 {
v >>= 1
log++
}
return log
}