// 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 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 }