Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions crates/ironrdp-egfx/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Client-side EGFX implementation
//!
//! This module provides client-side support for the Graphics Pipeline Extension
//! ([MS-RDPEGFX]), including H.264 AVC420 decode and surface management.
//! ([MS-RDPEGFX]), including H.264 AVC420 decode, ClearCodec decode, and surface management.
//!
//! # Protocol Compliance
//!
Expand All @@ -24,7 +24,7 @@
//! | |
//! | (For each frame:) |
//! |--- StartFrame ----------------------->|
//! |--- WireToSurface1 (H.264) ----------->| -> H264Decoder::decode()
//! |--- WireToSurface1 (codec) ----------->| -> H264/ClearCodec decode
//! |--- EndFrame ------------------------->| -> FrameAcknowledge
//! | |
//! |<---------- FrameAcknowledge ----------|
Expand Down Expand Up @@ -57,6 +57,7 @@ use std::collections::BTreeMap;

use ironrdp_core::{Decode as _, ReadCursor, impl_as_any};
use ironrdp_dvc::{DvcClientProcessor, DvcMessage, DvcProcessor};
use ironrdp_graphics::clearcodec::ClearCodecDecoder;
use ironrdp_graphics::zgfx;
use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _};
use ironrdp_pdu::{PduResult, decode_cursor, decode_err, pdu_other_err};
Expand Down Expand Up @@ -381,6 +382,7 @@ enum ClientState {
pub struct GraphicsPipelineClient {
handler: Box<dyn GraphicsPipelineHandler>,
h264_decoder: Option<Box<dyn H264Decoder>>,
clearcodec_decoder: ClearCodecDecoder,

decompressor: zgfx::Decompressor,
decompressed_buffer: Vec<u8>,
Expand All @@ -399,10 +401,12 @@ impl GraphicsPipelineClient {
/// Create a new `GraphicsPipelineClient`
///
/// If `h264_decoder` is `None`, AVC420 frames are logged and skipped.
/// ClearCodec decoding is always available (no external decoder required).
pub fn new(handler: Box<dyn GraphicsPipelineHandler>, h264_decoder: Option<Box<dyn H264Decoder>>) -> Self {
Self {
handler,
h264_decoder,
clearcodec_decoder: ClearCodecDecoder::new(),
decompressor: zgfx::Decompressor::new(),
decompressed_buffer: Vec::new(),
state: ClientState::WaitingForConfirm,
Expand Down Expand Up @@ -608,6 +612,9 @@ impl GraphicsPipelineClient {
if let Some(ref mut decoder) = self.h264_decoder {
decoder.reset();
}
// ClearCodec maintains V-bar and glyph caches that must be rebuilt
// after a graphics reset (the server will re-send all needed state).
self.clearcodec_decoder = ClearCodecDecoder::new();

debug!(width, height, "Graphics reset");
self.handler.on_reset_graphics(width, height);
Expand Down Expand Up @@ -694,6 +701,9 @@ impl GraphicsPipelineClient {
debug!("AVC444 codec not yet implemented, forwarding to handler");
self.handler.on_unhandled_pdu(&GfxPdu::WireToSurface1(pdu));
}
Codec1Type::ClearCodec => {
self.decode_clearcodec(pdu.surface_id, &pdu.destination_rectangle, &pdu.bitmap_data)?;
}
Codec1Type::Uncompressed => {
self.handle_uncompressed(pdu);
}
Expand Down Expand Up @@ -751,6 +761,36 @@ impl GraphicsPipelineClient {
Ok(())
}

fn decode_clearcodec(
&mut self,
surface_id: u16,
dest_rect: &InclusiveRectangle,
bitmap_data: &[u8],
) -> PduResult<()> {
let dest_width = dest_rect.width();
let dest_height = dest_rect.height();

let bgra = self
.clearcodec_decoder
.decode(bitmap_data, dest_width, dest_height)
.map_err(|e| pdu_other_err!("ClearCodec decode", source: e))?;

// ClearCodec outputs BGRA; convert to RGBA for the uniform BitmapUpdate format
let rgba = convert_bgra_to_rgba(&bgra);

let update = BitmapUpdate {
surface_id,
destination_rectangle: dest_rect.clone(),
codec_id: Codec1Type::ClearCodec,
data: rgba,
width: dest_width,
height: dest_height,
};

self.handler.on_bitmap_updated(&update);
Ok(())
}

fn handle_uncompressed(&mut self, pdu: crate::pdu::WireToSurface1Pdu) {
let dest_width = pdu.destination_rectangle.width();
let dest_height = pdu.destination_rectangle.height();
Expand Down Expand Up @@ -870,6 +910,19 @@ impl DvcClientProcessor for GraphicsPipelineClient {}
// Frame Cropping
// ============================================================================

/// Convert BGRA pixel data to RGBA8888
///
/// ClearCodec produces BGRA output per [MS-RDPEGFX 2.2.4.1]. Reorder to
/// [R, G, B, A] for the uniform `BitmapUpdate` pixel format.
fn convert_bgra_to_rgba(src: &[u8]) -> Vec<u8> {
debug_assert!(src.len() % 4 == 0, "BGRA input length not aligned to 4 bytes");
let mut dst = Vec::with_capacity(src.len());
for pixel in src.chunks_exact(4) {
dst.extend_from_slice(&[pixel[2], pixel[1], pixel[0], pixel[3]]);
}
dst
Comment thread
glamberson marked this conversation as resolved.
}

/// Convert uncompressed 32bpp little-endian pixels to RGBA8888
///
/// The wire format for uncompressed graphics is 0xAARRGGBB in a 32-bit
Expand Down Expand Up @@ -1031,6 +1084,24 @@ mod tests {
assert_eq!(cropped.len(), 1920 * 1080 * 4);
}

#[test]
fn convert_bgra_to_rgba_reorders_channels() {
// BGRA input: [B, G, R, A] per pixel
let bgra = vec![
0xFF, 0x00, 0x00, 0xCC, // B=255, G=0, R=0, A=204 (blue)
0x00, 0xFF, 0x00, 0x80, // B=0, G=255, R=0, A=128 (green)
];
let rgba = convert_bgra_to_rgba(&bgra);
// Expected: [R, G, B, A] per pixel
assert_eq!(
rgba,
vec![
0x00, 0x00, 0xFF, 0xCC, // R=0, G=0, B=255, A=204
0x00, 0xFF, 0x00, 0x80, // R=0, G=255, B=0, A=128
]
);
}

#[test]
fn convert_uncompressed_bgrx_to_rgba() {
// Wire format: [B, G, R, A] per pixel (0xAARRGGBB little-endian)
Expand Down
46 changes: 46 additions & 0 deletions crates/ironrdp-egfx/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,52 @@ impl GraphicsPipelineServer {
Some(frame_id)
}

/// Queue a ClearCodec frame for transmission.
///
/// ClearCodec is a mandatory lossless codec for all EGFX versions. It
/// provides excellent compression for text, UI elements, and icons.
///
/// `bitmap_data` should be a pre-encoded ClearCodec bitmap stream
/// (as produced by `ironrdp_graphics::clearcodec::ClearCodecEncoder`).
///
/// Returns `Some(frame_id)` if queued, `None` if backpressure is active
/// or the server is not ready.
pub fn send_clearcodec_frame(
&mut self,
surface_id: u16,
destination_rectangle: InclusiveRectangle,
bitmap_data: Vec<u8>,
timestamp_ms: u32,
) -> Option<u32> {
if !self.is_ready() {
return None;
}
if self.should_backpressure() {
Comment thread
glamberson marked this conversation as resolved.
self.qoe.record_backpressure();
return None;
}

let surface = self.surfaces.get(surface_id)?;

let timestamp = Self::make_timestamp(timestamp_ms);
let frame_id = self.frames.begin_frame(timestamp);

self.output_queue
.push_back(GfxPdu::StartFrame(StartFramePdu { timestamp, frame_id }));

self.output_queue.push_back(GfxPdu::WireToSurface1(WireToSurface1Pdu {
surface_id,
codec_id: Codec1Type::ClearCodec,
pixel_format: surface.pixel_format,
destination_rectangle,
bitmap_data,
}));

self.output_queue.push_back(GfxPdu::EndFrame(EndFramePdu { frame_id }));

Some(frame_id)
}

// ========================================================================
// Output Management
// ========================================================================
Expand Down
98 changes: 98 additions & 0 deletions crates/ironrdp-graphics/src/clearcodec/glyph_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! Glyph cache for ClearCodec (MS-RDPEGFX 2.2.4.1).
//!
//! When a bitmap area is <= 1024 pixels, ClearCodec can index it in a
//! 4,000-entry glyph cache. On a cache hit (FLAG_GLYPH_HIT), the previously
//! cached pixel data is reused without retransmission.

/// Maximum number of glyph cache entries.
pub const GLYPH_CACHE_SIZE: usize = 4_000;

/// A cached glyph entry: BGRA pixel data with dimensions.
#[derive(Debug, Clone)]
pub struct GlyphEntry {
pub width: u16,
pub height: u16,
/// BGRA pixel data (4 bytes per pixel).
pub pixels: Vec<u8>,
}

/// Glyph cache for ClearCodec bitmap deduplication.
pub struct GlyphCache {
entries: Vec<Option<GlyphEntry>>,
}

impl GlyphCache {
pub fn new() -> Self {
let mut entries = Vec::with_capacity(GLYPH_CACHE_SIZE);
entries.resize_with(GLYPH_CACHE_SIZE, || None);
Self { entries }
}

/// Look up a glyph by its cache index.
pub fn get(&self, index: u16) -> Option<&GlyphEntry> {
self.entries.get(usize::from(index)).and_then(|slot| slot.as_ref())
}

/// Store a glyph at the given index.
///
/// Returns `true` if the index was valid and the entry was stored.
pub fn store(&mut self, index: u16, entry: GlyphEntry) -> bool {
let idx = usize::from(index);
if idx < GLYPH_CACHE_SIZE {
self.entries[idx] = Some(entry);
true
} else {
false
}
}

/// Reset the entire glyph cache, removing all entries.
pub fn reset(&mut self) {
for slot in &mut self.entries {
*slot = None;
}
}
}

impl Default for GlyphCache {
fn default() -> Self {
Self::new()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn store_and_retrieve() {
let mut cache = GlyphCache::new();
let entry = GlyphEntry {
width: 8,
height: 16,
pixels: vec![0xFF; 8 * 16 * 4],
};
assert!(cache.store(42, entry));
let retrieved = cache.get(42).unwrap();
assert_eq!(retrieved.width, 8);
assert_eq!(retrieved.height, 16);
}

#[test]
fn get_empty_returns_none() {
let cache = GlyphCache::new();
assert!(cache.get(0).is_none());
assert!(cache.get(3999).is_none());
}

#[test]
fn reject_out_of_range() {
let mut cache = GlyphCache::new();
let entry = GlyphEntry {
width: 1,
height: 1,
pixels: vec![0; 4],
};
assert!(!cache.store(4000, entry));
}
}
Loading
Loading