322 lines
9.1 KiB
Go
322 lines
9.1 KiB
Go
package rdpgfx
|
||
|
||
// Non-progressive RemoteFX (RFX) codec decoder (MS-RDPRFX).
|
||
// Used for RDPGFX_CODECID_CAVIDEO (0x0003) in WIRE_TO_SURFACE_PDU_1.
|
||
//
|
||
// Block type codes (same numeric values as progressive, different semantics):
|
||
// 0xCCC0 WBT_SYNC
|
||
// 0xCCC1 WBT_CODEC_VERSIONS
|
||
// 0xCCC2 WBT_CHANNELS
|
||
// 0xCCC3 WBT_CONTEXT (+ 2-byte codecId/channelId)
|
||
// 0xCCC4 WBT_FRAME_BEGIN (+ 2-byte codecId/channelId)
|
||
// 0xCCC5 WBT_FRAME_END (+ 2-byte codecId/channelId)
|
||
// 0xCCC6 WBT_REGION (+ 2-byte codecId/channelId)
|
||
// 0xCCC7 WBT_EXTENSION (+ 2-byte codecId/channelId, contains TILESET)
|
||
//
|
||
// Tile sub-blocks inside TILESET use CBT_TILE (0xCAC3) with standard 6-byte header.
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"log/slog"
|
||
"runtime"
|
||
"sync"
|
||
)
|
||
|
||
const (
|
||
wbtSync = 0xCCC0
|
||
wbtCodecVersions = 0xCCC1
|
||
wbtChannels = 0xCCC2
|
||
wbtContext = 0xCCC3
|
||
wbtFrameBegin = 0xCCC4
|
||
wbtFrameEnd = 0xCCC5
|
||
wbtRegion = 0xCCC6
|
||
wbtExtension = 0xCCC7
|
||
|
||
cbtRegion = 0xCAC1
|
||
cbtTileset = 0xCAC2
|
||
cbtTile = 0xCAC3
|
||
)
|
||
|
||
type rfxTileWork struct {
|
||
content []byte
|
||
}
|
||
|
||
type rfxDecoder struct {
|
||
rectsBuf []rfxRect
|
||
tilesBuf []rfxTileWork
|
||
quantsBuf []rfxQuant
|
||
}
|
||
|
||
func newRfxDecoder() *rfxDecoder {
|
||
return &rfxDecoder{}
|
||
}
|
||
|
||
// Decode processes non-progressive RFX data, rendering tiles onto the
|
||
// provided surface buffer at the given (left, top) offset.
|
||
// Returns the bounding rectangles of decoded regions in surface coordinates.
|
||
func (d *rfxDecoder) Decode(data []byte, left, top int, surfData []byte, width, height int) []rfxRect {
|
||
var rects []rfxRect
|
||
var quants []rfxQuant
|
||
|
||
offset := 0
|
||
for offset+6 <= len(data) {
|
||
blockType := binary.LittleEndian.Uint16(data[offset:])
|
||
blockLen := int(binary.LittleEndian.Uint32(data[offset+2:]))
|
||
|
||
if blockLen < 6 || offset+blockLen > len(data) {
|
||
break
|
||
}
|
||
|
||
// Determine content start: blocks 0xCCC3-0xCCC7 have 2 extra bytes
|
||
// (codecId + channelId) per TS_RFX_CODEC_CHANNELT.
|
||
headerLen := 6
|
||
if blockType >= wbtContext && blockType <= wbtExtension {
|
||
headerLen = 8
|
||
}
|
||
|
||
if blockLen < headerLen {
|
||
break
|
||
}
|
||
content := data[offset+headerLen : offset+blockLen]
|
||
|
||
switch blockType {
|
||
case wbtSync, wbtCodecVersions, wbtChannels, wbtContext,
|
||
wbtFrameBegin, wbtFrameEnd:
|
||
// Infrastructure blocks — no action needed for decoding.
|
||
case wbtRegion:
|
||
rects = d.parseRegion(content, left, top)
|
||
case wbtExtension:
|
||
quants = d.decodeTileset(content, left, top, surfData, width, height)
|
||
}
|
||
|
||
offset += blockLen
|
||
}
|
||
|
||
// If no rects were parsed from REGION (e.g. numRects=0), generate one
|
||
// covering the entire surface per MS-RDPRFX 2.2.2.3.3.
|
||
if len(rects) == 0 && quants != nil {
|
||
rects = []rfxRect{{x: left, y: top, w: width - left, h: height - top}}
|
||
}
|
||
|
||
return rects
|
||
}
|
||
|
||
// parseRegion extracts rectangles from a WBT_REGION block.
|
||
// left/top are the WTS1 destination offsets applied to produce surface coordinates.
|
||
func (d *rfxDecoder) parseRegion(data []byte, left, top int) []rfxRect {
|
||
if len(data) < 7 {
|
||
return nil
|
||
}
|
||
|
||
// regionFlags := data[0]
|
||
numRects := binary.LittleEndian.Uint16(data[1:])
|
||
|
||
if numRects == 0 {
|
||
return nil
|
||
}
|
||
|
||
needed := 3 + int(numRects)*8 + 4
|
||
if len(data) < needed {
|
||
return nil
|
||
}
|
||
|
||
if cap(d.rectsBuf) >= int(numRects) {
|
||
d.rectsBuf = d.rectsBuf[:numRects]
|
||
} else {
|
||
d.rectsBuf = make([]rfxRect, numRects)
|
||
}
|
||
rects := d.rectsBuf
|
||
off := 3
|
||
for i := range numRects {
|
||
rects[i] = rfxRect{
|
||
x: left + int(binary.LittleEndian.Uint16(data[off:])),
|
||
y: top + int(binary.LittleEndian.Uint16(data[off+2:])),
|
||
w: int(binary.LittleEndian.Uint16(data[off+4:])),
|
||
h: int(binary.LittleEndian.Uint16(data[off+6:])),
|
||
}
|
||
off += 8
|
||
}
|
||
|
||
// Validate regionType
|
||
regionType := binary.LittleEndian.Uint16(data[off:])
|
||
if regionType != cbtRegion {
|
||
slog.Debug("RFX: unexpected regionType", "type", regionType)
|
||
}
|
||
|
||
return rects
|
||
}
|
||
|
||
// decodeTileset parses and decodes all tiles from a WBT_EXTENSION/TILESET block.
|
||
// Format: subtype(2) + idx(2) + properties(2) + numQuant(1) + tileSize(1) +
|
||
//
|
||
// numTiles(2) + tilesDataSize(4) + quants(numQuant*5) + tiles
|
||
//
|
||
// Returns the quant table for caller reference.
|
||
func (d *rfxDecoder) decodeTileset(data []byte, left, top int, surfData []byte, width, height int) []rfxQuant {
|
||
if len(data) < 14 {
|
||
return nil
|
||
}
|
||
|
||
subtype := binary.LittleEndian.Uint16(data[0:])
|
||
if subtype != cbtTileset {
|
||
return nil
|
||
}
|
||
|
||
properties := binary.LittleEndian.Uint16(data[4:])
|
||
numQuant := int(data[6])
|
||
// tileSize := data[7]
|
||
numTiles := int(binary.LittleEndian.Uint16(data[8:]))
|
||
// tilesDataSize := binary.LittleEndian.Uint32(data[10:])
|
||
|
||
// Extract RLGR entropy algorithm from TILESET properties.
|
||
// TILESET properties bit layout (MS-RDPRFX / FreeRDP):
|
||
// bits 10-13: et (entropy type) - 0x01=RLGR1, 0x04=RLGR3
|
||
rlgrMode := 1
|
||
et := (properties >> 10) & 0x0F
|
||
if et == 0x04 {
|
||
rlgrMode = 3
|
||
}
|
||
|
||
off := 14
|
||
|
||
// Parse quantization tables (5 bytes each, 10 nibbles)
|
||
if off+numQuant*5 > len(data) {
|
||
return nil
|
||
}
|
||
if cap(d.quantsBuf) >= numQuant {
|
||
d.quantsBuf = d.quantsBuf[:numQuant]
|
||
} else {
|
||
d.quantsBuf = make([]rfxQuant, numQuant)
|
||
}
|
||
quants := d.quantsBuf
|
||
for i := range numQuant {
|
||
quants[i] = parseRfxQuant(data[off:])
|
||
off += 5
|
||
}
|
||
|
||
// Collect tile content slices for parallel decoding.
|
||
if cap(d.tilesBuf) >= numTiles {
|
||
d.tilesBuf = d.tilesBuf[:0]
|
||
} else {
|
||
d.tilesBuf = make([]rfxTileWork, 0, numTiles)
|
||
}
|
||
tiles := d.tilesBuf
|
||
for range numTiles {
|
||
if off+6 > len(data) {
|
||
break
|
||
}
|
||
tileBlockType := binary.LittleEndian.Uint16(data[off:])
|
||
tileBlockLen := int(binary.LittleEndian.Uint32(data[off+2:]))
|
||
|
||
if tileBlockType != cbtTile {
|
||
break
|
||
}
|
||
if tileBlockLen < 19 || off+tileBlockLen > len(data) {
|
||
break
|
||
}
|
||
|
||
tiles = append(tiles, rfxTileWork{content: data[off+6 : off+tileBlockLen]})
|
||
off += tileBlockLen
|
||
}
|
||
d.tilesBuf = tiles
|
||
|
||
// Decode tiles concurrently — each tile writes to its own non-overlapping
|
||
// 64×64 region of the output buffer so no locking is needed. For small
|
||
// tile counts the goroutine + channel + WaitGroup overhead exceeds the
|
||
// per-tile work, so fall back to serial decoding below the threshold.
|
||
const parallelTileThreshold = 12
|
||
if len(tiles) >= parallelTileThreshold {
|
||
workers := min(runtime.NumCPU(), len(tiles))
|
||
ch := make(chan rfxTileWork, len(tiles))
|
||
for _, t := range tiles {
|
||
ch <- t
|
||
}
|
||
close(ch)
|
||
var wg sync.WaitGroup
|
||
for range workers {
|
||
wg.Go(func() {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
slog.Error("RFX: tile decode panic", "err", r)
|
||
}
|
||
}()
|
||
for t := range ch {
|
||
d.decodeTile(t.content, quants, rlgrMode, left, top, surfData, width, height, false)
|
||
}
|
||
})
|
||
}
|
||
wg.Wait()
|
||
} else {
|
||
for _, t := range tiles {
|
||
d.decodeTile(t.content, quants, rlgrMode, left, top, surfData, width, height, true)
|
||
}
|
||
}
|
||
|
||
return quants
|
||
}
|
||
|
||
// decodeTile decodes a single non-progressive RFX tile.
|
||
// Format: quantIdxY(1) + quantIdxCb(1) + quantIdxCr(1) + xIdx(2) + yIdx(2) +
|
||
//
|
||
// YLen(2) + CbLen(2) + CrLen(2) + YData(YLen) + CbData(CbLen) + CrData(CrLen)
|
||
//
|
||
// When parallelComponents is true the Y, Cb, and Cr channels are decoded
|
||
// concurrently (safe because each works on its own independent data and pool
|
||
// buffer). Use true for the serial-tile path; false when the outer worker pool
|
||
// already saturates all CPUs.
|
||
func (d *rfxDecoder) decodeTile(data []byte, quants []rfxQuant, rlgrMode int, left, top int, output []byte, outW, outH int, parallelComponents bool) {
|
||
if len(data) < 13 {
|
||
return
|
||
}
|
||
|
||
quantIdxY := int(data[0])
|
||
quantIdxCb := int(data[1])
|
||
quantIdxCr := int(data[2])
|
||
xIdx := int(binary.LittleEndian.Uint16(data[3:]))
|
||
yIdx := int(binary.LittleEndian.Uint16(data[5:]))
|
||
yLen := int(binary.LittleEndian.Uint16(data[7:]))
|
||
cbLen := int(binary.LittleEndian.Uint16(data[9:]))
|
||
crLen := int(binary.LittleEndian.Uint16(data[11:]))
|
||
|
||
off := 13
|
||
yData := safeSlice(data, off, yLen)
|
||
off += yLen
|
||
cbData := safeSlice(data, off, cbLen)
|
||
off += cbLen
|
||
crData := safeSlice(data, off, crLen)
|
||
|
||
qY := rfxGetQuant(quants, quantIdxY)
|
||
qCb := rfxGetQuant(quants, quantIdxCb)
|
||
qCr := rfxGetQuant(quants, quantIdxCr)
|
||
|
||
var yPixels, cbPixels, crPixels []int16
|
||
if parallelComponents {
|
||
var wg sync.WaitGroup
|
||
wg.Go(func() { yPixels = rfxDecodeComponent(yData, qY, rlgrMode) })
|
||
wg.Go(func() { cbPixels = rfxDecodeComponent(cbData, qCb, rlgrMode) })
|
||
wg.Go(func() { crPixels = rfxDecodeComponent(crData, qCr, rlgrMode) })
|
||
wg.Wait()
|
||
} else {
|
||
yPixels = rfxDecodeComponent(yData, qY, rlgrMode)
|
||
cbPixels = rfxDecodeComponent(cbData, qCb, rlgrMode)
|
||
crPixels = rfxDecodeComponent(crData, qCr, rlgrMode)
|
||
}
|
||
|
||
// Apply WTS1 left/top offset: tile pixel position on surface =
|
||
// left + xIdx*64, top + yIdx*64 (per FreeRDP/MS-RDPRFX).
|
||
rfxPlaceTileAbs(yPixels, cbPixels, crPixels, left+xIdx*rfxTileSize, top+yIdx*rfxTileSize, output, outW, outH)
|
||
|
||
coeffPool.Put((*coeffArr)(yPixels))
|
||
coeffPool.Put((*coeffArr)(cbPixels))
|
||
coeffPool.Put((*coeffArr)(crPixels))
|
||
}
|
||
|
||
// DecodeSurfaceRFX decodes non-progressive RemoteFX (MS-RDPRFX) encoded data
|
||
// into a top-down BGRA pixel buffer suitable for surface bitmap commands.
|
||
func DecodeSurfaceRFX(data []byte, width, height int) []byte {
|
||
output := make([]byte, width*height*4)
|
||
dec := newRfxDecoder()
|
||
dec.Decode(data, 0, 0, output, width, height)
|
||
return output
|
||
}
|