Files
rdplib/plugin/rdpgfx/rfx.go
T

322 lines
9.1 KiB
Go
Raw 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.
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
}