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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/ironrdp-egfx/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#![cfg_attr(doc, doc = include_str!("../README.md"))]
#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")]

pub(crate) const CHANNEL_NAME: &str = "Microsoft::Windows::RDS::Graphics";
/// EGFX dynamic virtual channel name per MS-RDPEGFX
pub const CHANNEL_NAME: &str = "Microsoft::Windows::RDS::Graphics";

pub mod client;
pub mod pdu;
Expand Down
116 changes: 108 additions & 8 deletions crates/ironrdp-egfx/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@
use std::collections::{HashMap, VecDeque};
use std::time::Instant;

use ironrdp_core::{decode, impl_as_any};
use ironrdp_dvc::{DvcMessage, DvcProcessor, DvcServerProcessor};
use ironrdp_core::{decode, impl_as_any, Encode, EncodeResult, WriteCursor};
use ironrdp_dvc::{DvcEncode, DvcMessage, DvcProcessor, DvcServerProcessor};
use ironrdp_graphics::zgfx::wrap_uncompressed;
use ironrdp_pdu::gcc::Monitor;
use ironrdp_pdu::geometry::InclusiveRectangle;
use ironrdp_pdu::{decode_err, PduResult};
Expand All @@ -85,6 +86,32 @@ const DEFAULT_MAX_FRAMES_IN_FLIGHT: u32 = 3;
/// Special queue depth value indicating client has disabled acknowledgments
const SUSPEND_FRAME_ACK_QUEUE_DEPTH: u32 = 0xFFFFFFFF;

/// Pre-encoded ZGFX-wrapped bytes for DVC transmission.
///
/// `Encode::encode()` takes `&self`, but ZGFX wrapping is done in `drain_output()`
/// where `&mut self` is available. This type holds the already-wrapped bytes.
struct ZgfxWrappedBytes {
bytes: Vec<u8>,
pdu_name: &'static str,
}

impl Encode for ZgfxWrappedBytes {
fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
dst.write_slice(&self.bytes);
Ok(())
}

fn name(&self) -> &'static str {
self.pdu_name
}

fn size(&self) -> usize {
self.bytes.len()
}
}

impl DvcEncode for ZgfxWrappedBytes {}

// ============================================================================
// Surface Management
// ============================================================================
Expand Down Expand Up @@ -607,7 +634,12 @@ pub struct GraphicsPipelineServer {

output_width: u16,
output_height: u16,
/// MS-RDPEGFX requires ResetGraphics before any CreateSurface
reset_graphics_sent: bool,
output_queue: VecDeque<GfxPdu>,

/// Stored from DvcProcessor::start() for proactive frame encoding
channel_id: Option<u32>,
}

impl GraphicsPipelineServer {
Expand All @@ -626,10 +658,29 @@ impl GraphicsPipelineServer {
frames,
output_width: 0,
output_height: 0,
reset_graphics_sent: false,
output_queue: VecDeque::new(),
channel_id: None,
}
}

/// Set desktop output dimensions for ResetGraphics.
///
/// Call before `create_surface()` when the desktop size differs from
/// the surface size (e.g. 16-pixel alignment padding).
pub fn set_output_dimensions(&mut self, width: u16, height: u16) {
self.output_width = width;
self.output_height = height;
}

/// DVC channel ID assigned by DRDYNVC.
///
/// Returns `None` before the channel has been started.
#[must_use]
pub fn channel_id(&self) -> Option<u32> {
self.channel_id
}

// ========================================================================
// State Queries
// ========================================================================
Expand Down Expand Up @@ -688,6 +739,31 @@ impl GraphicsPipelineServer {
return None;
}

// MS-RDPEGFX: ResetGraphics MUST precede any CreateSurface.
// Auto-send on first surface creation if not explicitly sent via resize().
if !self.reset_graphics_sent {
let desktop_width = if self.output_width > 0 {
self.output_width
} else {
width
};
let desktop_height = if self.output_height > 0 {
self.output_height
} else {
height
};

self.output_queue.push_back(GfxPdu::ResetGraphics(ResetGraphicsPdu {
width: u32::from(desktop_width),
height: u32::from(desktop_height),
monitors: Vec::new(),
}));

self.output_width = desktop_width;
self.output_height = desktop_height;
self.reset_graphics_sent = true;
}

let surface_id = self.surfaces.allocate_id();
let surface = Surface::new(surface_id, width, height, pixel_format);

Expand Down Expand Up @@ -804,6 +880,7 @@ impl GraphicsPipelineServer {
monitors,
}));

self.reset_graphics_sent = true;
self.state = ServerState::Ready;
}

Expand Down Expand Up @@ -1016,14 +1093,34 @@ impl GraphicsPipelineServer {
// Output Management
// ========================================================================

/// Drain the output queue and return PDUs to send
/// Drain the output queue, ZGFX-wrapping each PDU for DVC transmission.
///
/// Call this method to get pending PDUs that need to be sent to the client.
/// Each `GfxPdu` is encoded to bytes then wrapped in uncompressed ZGFX
/// segment format. Windows clients expect this wrapping on the EGFX DVC.
///
/// # Panics
///
/// Panics if a `GfxPdu` fails to encode. This indicates a bug in the PDU
/// encoding logic, not a runtime condition.
#[expect(clippy::as_conversions, reason = "Box<T> to Box<dyn Trait> coercion")]
pub fn drain_output(&mut self) -> Vec<DvcMessage> {
self.output_queue
.drain(..)
.map(|pdu| Box::new(pdu) as DvcMessage)
.map(|pdu| {
let pdu_name = pdu.name();
let pdu_size = pdu.size();
let mut pdu_bytes = vec![0u8; pdu_size];
let mut cursor = WriteCursor::new(&mut pdu_bytes);
pdu.encode(&mut cursor).expect("GfxPdu encoding should not fail");

let wrapped = wrap_uncompressed(&pdu_bytes);
trace!(pdu_name, pdu_size, wrapped = wrapped.len(), "ZGFX wrapped");

Box::new(ZgfxWrappedBytes {
bytes: wrapped,
pdu_name,
}) as DvcMessage
})
.collect()
}

Expand Down Expand Up @@ -1099,13 +1196,16 @@ impl DvcProcessor for GraphicsPipelineServer {
CHANNEL_NAME
}

fn start(&mut self, _channel_id: u32) -> PduResult<Vec<DvcMessage>> {
// Server waits for client CapabilitiesAdvertise before sending anything
fn start(&mut self, channel_id: u32) -> PduResult<Vec<DvcMessage>> {
self.channel_id = Some(channel_id);
debug!(channel_id, "EGFX channel started");
Ok(vec![])
}

fn close(&mut self, _channel_id: u32) {
debug!("EGFX channel closed");
self.state = ServerState::Closed;
self.reset_graphics_sent = false;
self.handler.on_close();
}

Expand Down Expand Up @@ -1142,7 +1242,7 @@ impl DvcServerProcessor for GraphicsPipelineServer {}

/// Encode an AVC444 bitmap stream to bytes
fn encode_avc444_bitmap_stream(stream: &Avc444BitmapStream<'_>) -> Vec<u8> {
use ironrdp_pdu::{Encode as _, WriteCursor};
use ironrdp_pdu::Encode as _;

let size = stream.size();
let mut buf = vec![0u8; size];
Expand Down
2 changes: 2 additions & 0 deletions crates/ironrdp-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ helper = ["dep:x509-cert", "dep:rustls-pemfile"]
rayon = ["dep:rayon"]
qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"]
qoiz = ["dep:zstd-safe", "qoi", "ironrdp-pdu/qoiz"]
egfx = ["dep:ironrdp-egfx"]

# Internal (PRIVATE!) features used to aid testing.
# Don't rely on these whatsoever. They may disappear at any time.
Expand Down Expand Up @@ -51,6 +52,7 @@ bytes = "1"
visibility = { version = "0.1", optional = true }
qoicoubeh = { version = "0.5", optional = true }
zstd-safe = { version = "7.2", optional = true }
ironrdp-egfx = { path = "../ironrdp-egfx", version = "0.1", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["sync"] }
Expand Down
17 changes: 17 additions & 0 deletions crates/ironrdp-server/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use tokio_rustls::TlsAcceptor;

use super::clipboard::CliprdrServerFactory;
use super::display::{DesktopSize, RdpServerDisplay};
#[cfg(feature = "egfx")]
use super::gfx::GfxServerFactory;
use super::handler::{KeyboardEvent, MouseEvent, RdpServerInputHandler};
use super::server::{RdpServer, RdpServerOptions, RdpServerSecurity};
use crate::{DisplayUpdate, RdpServerDisplayUpdates, SoundServerFactory};
Expand All @@ -31,6 +33,8 @@ pub struct BuilderDone {
display: Box<dyn RdpServerDisplay>,
cliprdr_factory: Option<Box<dyn CliprdrServerFactory>>,
sound_factory: Option<Box<dyn SoundServerFactory>>,
#[cfg(feature = "egfx")]
gfx_factory: Option<Box<dyn GfxServerFactory>>,
}

pub struct RdpServerBuilder<State> {
Expand Down Expand Up @@ -124,6 +128,8 @@ impl RdpServerBuilder<WantsDisplay> {
sound_factory: None,
cliprdr_factory: None,
codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"),
#[cfg(feature = "egfx")]
gfx_factory: None,
},
}
}
Expand All @@ -138,6 +144,8 @@ impl RdpServerBuilder<WantsDisplay> {
sound_factory: None,
cliprdr_factory: None,
codecs: server_codecs_capabilities(&[]).expect("can't panic for &[]"),
#[cfg(feature = "egfx")]
gfx_factory: None,
},
}
}
Expand All @@ -154,6 +162,13 @@ impl RdpServerBuilder<BuilderDone> {
self
}

/// Configure EGFX (Graphics Pipeline Extension) for H.264 video streaming.
#[cfg(feature = "egfx")]
pub fn with_gfx_factory(mut self, gfx_factory: Option<Box<dyn GfxServerFactory>>) -> Self {
self.state.gfx_factory = gfx_factory;
self
}

pub fn with_bitmap_codecs(mut self, codecs: BitmapCodecs) -> Self {
self.state.codecs = codecs;
self
Expand All @@ -170,6 +185,8 @@ impl RdpServerBuilder<BuilderDone> {
self.state.display,
self.state.sound_factory,
self.state.cliprdr_factory,
#[cfg(feature = "egfx")]
self.state.gfx_factory,
)
}
}
Expand Down
108 changes: 108 additions & 0 deletions crates/ironrdp-server/src/gfx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! EGFX (Graphics Pipeline Extension) server integration.
//!
//! Provides the bridge between `ironrdp-egfx`'s `GraphicsPipelineServer` and
//! `ironrdp-server`'s `RdpServer`, enabling H.264 video streaming via DVC.
//!
//! The bridge pattern (`GfxDvcBridge`) wraps an `Arc<Mutex<GraphicsPipelineServer>>`
//! so the display handler can call `send_avc420_frame()` proactively while the
//! DVC infrastructure handles client messages (capability negotiation, frame acks).

use std::sync::{Arc, Mutex};

use ironrdp_core::impl_as_any;
use ironrdp_dvc::{DvcMessage, DvcProcessor, DvcServerProcessor};
use ironrdp_egfx::server::{GraphicsPipelineHandler, GraphicsPipelineServer};
use ironrdp_pdu::PduResult;
use ironrdp_svc::SvcMessage;

use crate::server::ServerEventSender;

/// Shared handle to a `GraphicsPipelineServer`.
///
/// Uses `std::sync::Mutex` (not tokio) because `DvcProcessor` trait methods
/// are synchronous and cannot hold async locks.
pub type GfxServerHandle = Arc<Mutex<GraphicsPipelineServer>>;

/// Factory for creating EGFX graphics pipeline handlers.
///
/// Implements `ServerEventSender` so the factory can signal the server event loop
/// when EGFX frames are ready to be drained and sent.
pub trait GfxServerFactory: ServerEventSender + Send {
/// Create a handler for EGFX callbacks (caps negotiation, frame acks).
fn build_gfx_handler(&self) -> Box<dyn GraphicsPipelineHandler>;

/// Create a bridge and shared server handle for proactive frame sending.
///
/// When returning `Some`, the bridge is registered with DrdynvcServer for
/// client messages, and the handle is available for direct frame submission.
/// Returns `None` by default, falling back to `build_gfx_handler()`.
fn build_server_with_handle(&self) -> Option<(GfxDvcBridge, GfxServerHandle)> {
None
}
}

/// DVC bridge wrapping a shared `GraphicsPipelineServer`.
///
/// Delegates all `DvcProcessor` methods to the inner server through a mutex,
/// enabling shared access from both the DVC layer and the display handler.
pub struct GfxDvcBridge {
inner: GfxServerHandle,
}

impl GfxDvcBridge {
pub fn new(server: GfxServerHandle) -> Self {
Self { inner: server }
}

pub fn server(&self) -> &GfxServerHandle {
&self.inner
}
}

impl_as_any!(GfxDvcBridge);

impl DvcProcessor for GfxDvcBridge {
fn channel_name(&self) -> &str {
ironrdp_egfx::CHANNEL_NAME
}

fn start(&mut self, channel_id: u32) -> PduResult<Vec<DvcMessage>> {
self.inner
.lock()
.expect("GfxServerHandle mutex poisoned")
.start(channel_id)
}

fn process(&mut self, channel_id: u32, payload: &[u8]) -> PduResult<Vec<DvcMessage>> {
self.inner
.lock()
.expect("GfxServerHandle mutex poisoned")
.process(channel_id, payload)
}

fn close(&mut self, channel_id: u32) {
self.inner
.lock()
.expect("GfxServerHandle mutex poisoned")
.close(channel_id)
}
}

impl DvcServerProcessor for GfxDvcBridge {}

/// Message for routing EGFX PDUs to the wire via `ServerEvent`.
#[derive(Debug)]
pub enum EgfxServerMessage {
/// Pre-encoded DVC messages from `GraphicsPipelineServer::drain_output()`.
SendMessages { messages: Vec<SvcMessage> },
}

impl core::fmt::Display for EgfxServerMessage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::SendMessages { messages } => {
write!(f, "SendMessages(count={})", messages.len())
}
}
}
}
Loading
Loading