init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib

This commit is contained in:
w11
2026-09-19 18:55:43 +08:00
commit 1096fbce9d
94 changed files with 36790 additions and 0 deletions
+710
View File
@@ -0,0 +1,710 @@
// 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
}