From 797a80c37cda03dd9c6461ec6ac308e4e16cfd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Tue, 22 Jul 2025 17:14:53 +0400 Subject: [PATCH 1/5] chore(bench): fix could not find `time` in `tokio` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marc-André Lureau --- benches/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/Cargo.toml b/benches/Cargo.toml index b5f69ca24..8124d3372 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -19,7 +19,7 @@ ironrdp = { path = "../crates/ironrdp", features = [ "__bench", ] } pico-args = "0.5.0" -tokio = { version = "1", features = ["sync", "fs"] } +tokio = { version = "1", features = ["sync", "fs", "time"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing = { version = "0.1", features = ["log"] } From 2fb69ffd5a03d1d49d28e64539af3c733bc4dd5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Wed, 5 Mar 2025 15:08:25 +0400 Subject: [PATCH 2/5] refactor(session): generalize apply_rgb24() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional "flip" argument for inverting bitmaps. Signed-off-by: Marc-André Lureau --- crates/ironrdp-session/src/fast_path.rs | 2 +- crates/ironrdp-session/src/image.rs | 52 ++++++++++++++++--------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/crates/ironrdp-session/src/fast_path.rs b/crates/ironrdp-session/src/fast_path.rs index d0a25da0b..59db1326b 100644 --- a/crates/ironrdp-session/src/fast_path.rs +++ b/crates/ironrdp-session/src/fast_path.rs @@ -114,7 +114,7 @@ impl Processor { usize::from(update.width), usize::from(update.height), ) { - Ok(()) => image.apply_rgb24_bitmap(&buf, &update.rectangle)?, + Ok(()) => image.apply_rgb24(&buf, &update.rectangle, true)?, Err(err) => { warn!("Invalid RDP6_BITMAP_STREAM: {err}"); update.rectangle.clone() diff --git a/crates/ironrdp-session/src/image.rs b/crates/ironrdp-session/src/image.rs index 138b3dfb3..a2c8c78e2 100644 --- a/crates/ironrdp-session/src/image.rs +++ b/crates/ironrdp-session/src/image.rs @@ -570,43 +570,57 @@ impl DecodedImage { } // FIXME: this assumes PixelFormat::RgbA32 - pub(crate) fn apply_rgb24_bitmap( + fn apply_rgb24_iter<'a, I>( &mut self, - rgb24: &[u8], + rgb24: I, update_rectangle: &InclusiveRectangle, - ) -> SessionResult { + ) -> SessionResult + where + I: Iterator, + { const SRC_COLOR_DEPTH: usize = 3; const DST_COLOR_DEPTH: usize = 4; let image_width = self.width as usize; - let rectangle_width = usize::from(update_rectangle.width()); let top = usize::from(update_rectangle.top); let left = usize::from(update_rectangle.left); let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; - rgb24 - .chunks_exact(rectangle_width * SRC_COLOR_DEPTH) - .rev() - .enumerate() - .for_each(|(row_idx, row)| { - row.chunks_exact(SRC_COLOR_DEPTH) - .enumerate() - .for_each(|(col_idx, src_pixel)| { - let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + rgb24.enumerate().for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; - // Copy RGB channels as is - self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel); - // Set alpha channel to opaque(0xFF) - self.data[dst_idx + 3] = 0xFF; - }) - }); + // Copy RGB channels as is + self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel); + // Set alpha channel to opaque(0xFF) + self.data[dst_idx + 3] = 0xFF; + }) + }); let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; Ok(update_rectangle) } + pub(crate) fn apply_rgb24( + &mut self, + rgb24: &[u8], + update_rectangle: &InclusiveRectangle, + flip: bool, + ) -> SessionResult { + const SRC_COLOR_DEPTH: usize = 3; + let rectangle_width = usize::from(update_rectangle.width()); + let lines = rgb24.chunks_exact(rectangle_width * SRC_COLOR_DEPTH); + if flip { + self.apply_rgb24_iter(lines.rev(), update_rectangle) + } else { + self.apply_rgb24_iter(lines, update_rectangle) + } + } + // FIXME: this assumes PixelFormat::RgbA32 pub(crate) fn apply_rgb32_bitmap( &mut self, From e09fdc4bab272a33cea0690f37dd2e3f4f2c791a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Tue, 18 Mar 2025 19:12:11 +0400 Subject: [PATCH 3/5] feat(server)!: add server_codecs_capabilities() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Teach the server to support customizable codecs set. Use the same logic/parsing as the client codecs configuration. Replace "with_remote_fx" with "codecs". Signed-off-by: Marc-André Lureau --- crates/ironrdp-pdu/src/rdp/capability_sets.rs | 6 +- .../src/rdp/capability_sets/bitmap_codecs.rs | 100 +++++++++++++----- crates/ironrdp-server/src/builder.rs | 13 +-- crates/ironrdp-server/src/capabilities.rs | 19 +--- crates/ironrdp-server/src/server.rs | 34 ++++-- 5 files changed, 110 insertions(+), 62 deletions(-) diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets.rs b/crates/ironrdp-pdu/src/rdp/capability_sets.rs index 38bb5a39e..6dd189a01 100644 --- a/crates/ironrdp-pdu/src/rdp/capability_sets.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets.rs @@ -32,9 +32,9 @@ pub use self::bitmap_cache::{ BitmapCache, BitmapCacheRev2, CacheEntry, CacheFlags, CellInfo, BITMAP_CACHE_ENTRIES_NUM, }; pub use self::bitmap_codecs::{ - client_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty, EntropyBits, Guid, NsCodec, - RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags, CODEC_ID_NONE, - CODEC_ID_REMOTEFX, + client_codecs_capabilities, server_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty, + EntropyBits, Guid, NsCodec, RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags, + CODEC_ID_NONE, CODEC_ID_REMOTEFX, }; pub use self::brush::{Brush, SupportLevel}; pub use self::frame_acknowledge::FrameAcknowledge; diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs index 32162e944..01a7a0eeb 100644 --- a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs @@ -2,6 +2,7 @@ mod tests; use core::fmt::{self, Debug}; +use std::collections::HashMap; use bitflags::bitflags; use ironrdp_core::{ @@ -641,6 +642,30 @@ impl CodecId { } } +fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result, String> { + let mut result = HashMap::new(); + + for &codec_str in codecs { + if let Some(colon_index) = codec_str.find(':') { + let codec_name = &codec_str[0..colon_index]; + let state_str = &codec_str[colon_index + 1..]; + + let state = match state_str { + "on" => true, + "off" => false, + _ => return Err(format!("Unhandled configuration: {state_str}")), + }; + + result.insert(codec_name, state); + } else { + // No colon found, assume it's "on" + result.insert(codec_str, true); + } + } + + Ok(result) +} + /// This function generates a list of client codec capabilities based on the /// provided configuration. /// @@ -659,32 +684,6 @@ impl CodecId { /// A vector of `Codec` structs representing the codec capabilities, or an error /// suitable for CLI. pub fn client_codecs_capabilities(config: &[&str]) -> Result { - use std::collections::HashMap; - - fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result, String> { - let mut result = HashMap::new(); - - for &codec_str in codecs { - if let Some(colon_index) = codec_str.find(':') { - let codec_name = &codec_str[0..colon_index]; - let state_str = &codec_str[colon_index + 1..]; - - let state = match state_str { - "on" => true, - "off" => false, - _ => return Err(format!("Unhandled configuration: {state_str}")), - }; - - result.insert(codec_name, state); - } else { - // No colon found, assume it's "on" - result.insert(codec_str, true); - } - } - - Ok(result) - } - if config.contains(&"help") { return Err(r#" List of codecs: @@ -692,6 +691,7 @@ List of codecs: "# .to_owned()); } + let mut config = parse_codecs_config(config)?; let mut codecs = vec![]; @@ -715,3 +715,51 @@ List of codecs: Ok(BitmapCodecs(codecs)) } + +/// This function generates a list of server codec capabilities based on the +/// provided configuration. +/// +/// # Arguments +/// +/// * `config` - A slice of string slices that specifies which codecs to include +/// in the capabilities. Codecs can be explicitly turned on ("codec:on") or +/// off ("codec:off"). +/// +/// # List of codecs +/// +/// * `remotefx` (on by default) +/// +/// # Returns +/// +/// A vector of `Codec` structs representing the codec capabilities, or an help message suitable +/// for CLI errors. +pub fn server_codecs_capabilities(config: &[&str]) -> Result { + if config.contains(&"help") { + return Err(r#" +List of codecs: +- `remotefx` (on by default) +"# + .to_owned()); + } + + let mut config = parse_codecs_config(config)?; + let mut codecs = vec![]; + + if config.remove("remotefx").unwrap_or(true) { + codecs.push(Codec { + id: 0, + property: CodecProperty::RemoteFx(RemoteFxContainer::ServerContainer(1)), + }); + codecs.push(Codec { + id: 0, + property: CodecProperty::ImageRemoteFx(RemoteFxContainer::ServerContainer(1)), + }); + } + + let codec_names = config.keys().copied().collect::>().join(", "); + if !codec_names.is_empty() { + return Err(format!("Unknown codecs: {codec_names}")); + } + + Ok(BitmapCodecs(codecs)) +} diff --git a/crates/ironrdp-server/src/builder.rs b/crates/ironrdp-server/src/builder.rs index d31aedbac..c8e7b9e2a 100644 --- a/crates/ironrdp-server/src/builder.rs +++ b/crates/ironrdp-server/src/builder.rs @@ -1,6 +1,7 @@ use core::net::SocketAddr; use anyhow::Result; +use ironrdp_pdu::rdp::capability_sets::{server_codecs_capabilities, BitmapCodecs}; use tokio_rustls::TlsAcceptor; use super::clipboard::CliprdrServerFactory; @@ -25,7 +26,7 @@ pub struct WantsDisplay { pub struct BuilderDone { addr: SocketAddr, security: RdpServerSecurity, - with_remote_fx: bool, + codecs: BitmapCodecs, handler: Box, display: Box, cliprdr_factory: Option>, @@ -124,7 +125,7 @@ impl RdpServerBuilder { display: Box::new(display), sound_factory: None, cliprdr_factory: None, - with_remote_fx: true, + codecs: server_codecs_capabilities(&[]).unwrap(), }, } } @@ -138,7 +139,7 @@ impl RdpServerBuilder { display: Box::new(NoopDisplay), sound_factory: None, cliprdr_factory: None, - with_remote_fx: true, + codecs: server_codecs_capabilities(&[]).unwrap(), }, } } @@ -155,8 +156,8 @@ impl RdpServerBuilder { self } - pub fn with_remote_fx(mut self, enabled: bool) -> Self { - self.state.with_remote_fx = enabled; + pub fn with_bitmap_codecs(mut self, codecs: BitmapCodecs) -> Self { + self.state.codecs = codecs; self } @@ -165,7 +166,7 @@ impl RdpServerBuilder { RdpServerOptions { addr: self.state.addr, security: self.state.security, - with_remote_fx: self.state.with_remote_fx, + codecs: self.state.codecs, }, self.state.handler, self.state.display, diff --git a/crates/ironrdp-server/src/capabilities.rs b/crates/ironrdp-server/src/capabilities.rs index 0e2df9345..5a7cc8ea4 100644 --- a/crates/ironrdp-server/src/capabilities.rs +++ b/crates/ironrdp-server/src/capabilities.rs @@ -12,7 +12,7 @@ pub(crate) fn capabilities(opts: &RdpServerOptions, size: DesktopSize) -> Vec capability_sets::MultifragmentUpdate { max_request_size: 16_777_215, } } - -fn bitmap_codecs(with_remote_fx: bool) -> capability_sets::BitmapCodecs { - let mut codecs = Vec::new(); - if with_remote_fx { - codecs.push(capability_sets::Codec { - id: 0, - property: capability_sets::CodecProperty::RemoteFx(capability_sets::RemoteFxContainer::ServerContainer(1)), - }); - codecs.push(capability_sets::Codec { - id: 0, - property: capability_sets::CodecProperty::ImageRemoteFx( - capability_sets::RemoteFxContainer::ServerContainer(1), - ), - }); - } - capability_sets::BitmapCodecs(codecs) -} diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index c2a663a0e..0acb7603b 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -13,7 +13,7 @@ use ironrdp_displaycontrol::server::{DisplayControlHandler, DisplayControlServer use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; use ironrdp_pdu::input::InputEventPdu; use ironrdp_pdu::mcs::{SendDataIndication, SendDataRequest}; -use ironrdp_pdu::rdp::capability_sets::{BitmapCodecs, CapabilitySet, CmdFlags, GeneralExtraFlags}; +use ironrdp_pdu::rdp::capability_sets::{BitmapCodecs, CapabilitySet, CmdFlags, CodecProperty, GeneralExtraFlags}; pub use ironrdp_pdu::rdp::client_info::Credentials; use ironrdp_pdu::rdp::headers::{ServerDeactivateAll, ShareControlPdu}; use ironrdp_pdu::x224::X224; @@ -38,7 +38,23 @@ use crate::{builder, capabilities, SoundServerFactory}; pub struct RdpServerOptions { pub addr: SocketAddr, pub security: RdpServerSecurity, - pub with_remote_fx: bool, + pub codecs: BitmapCodecs, +} + +impl RdpServerOptions { + fn has_image_remote_fx(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::ImageRemoteFx(_))) + } + + fn has_remote_fx(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::RemoteFx(_))) + } } #[derive(Clone)] @@ -711,21 +727,21 @@ impl RdpServer { // We should distinguish parameters for both modes, // and somehow choose the "best", instead of picking // the last parsed here. - rdp::capability_sets::CodecProperty::RemoteFx( - rdp::capability_sets::RemoteFxContainer::ClientContainer(c), - ) if self.opts.with_remote_fx => { + CodecProperty::RemoteFx(rdp::capability_sets::RemoteFxContainer::ClientContainer(c)) + if self.opts.has_remote_fx() => + { for caps in c.caps_data.0 .0 { update_codecs.set_remotefx(Some((caps.entropy_bits, codec.id))); } } - rdp::capability_sets::CodecProperty::ImageRemoteFx( - rdp::capability_sets::RemoteFxContainer::ClientContainer(c), - ) if self.opts.with_remote_fx => { + CodecProperty::ImageRemoteFx(rdp::capability_sets::RemoteFxContainer::ClientContainer( + c, + )) if self.opts.has_image_remote_fx() => { for caps in c.caps_data.0 .0 { update_codecs.set_remotefx(Some((caps.entropy_bits, codec.id))); } } - rdp::capability_sets::CodecProperty::NsCodec(_) => (), + CodecProperty::NsCodec(_) => (), _ => (), } } From c7f190a48e854cf9a208976ce0d043868e24dd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Wed, 5 Mar 2025 15:10:02 +0400 Subject: [PATCH 4/5] feat: add QOI image codec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Quite OK Image format ([1]) losslessly compresses images to a similar size of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding. Add a new QOI codec (UUID 4dae9af8-b399-4df6-b43a-662fd9c0f5d6) for SetSurface command. The PDU data contains the QOI header (14 bytes) + data "chunks" and the end marker (8 bytes). Some benchmarks showing interesting results (using ironrdp/perfenc) Bitmap: 74s user CPU, 92.5% compression RemoteFx (lossy): 201s user CPU, 96.72% compression QOI: 10s user CPU, 96.20% compression Note: the "qoicoubeh" crate is my own fork of "qoi-rust" project. The plan is to switch back to it as soon as the maintainer resume its activites (https://github.com/aldanor/qoi-rust/issues/14). [1]: https://qoiformat.org/ Signed-off-by: Marc-André Lureau --- Cargo.lock | 11 ++++ benches/Cargo.toml | 4 ++ benches/src/perfenc.rs | 8 ++- crates/ironrdp-client/Cargo.toml | 1 + crates/ironrdp-connector/Cargo.toml | 2 + crates/ironrdp-pdu/Cargo.toml | 1 + crates/ironrdp-pdu/src/rdp/capability_sets.rs | 2 +- .../src/rdp/capability_sets/bitmap_codecs.rs | 41 +++++++++++++ crates/ironrdp-server/Cargo.toml | 4 +- crates/ironrdp-server/src/encoder/mod.rs | 61 ++++++++++++++++++- crates/ironrdp-server/src/server.rs | 12 ++++ crates/ironrdp-session/Cargo.toml | 6 +- crates/ironrdp-session/src/fast_path.rs | 17 ++++++ crates/ironrdp-testsuite-core/Cargo.toml | 2 +- .../tests/session/mod.rs | 18 +++++- crates/ironrdp-web/Cargo.toml | 3 +- crates/ironrdp/Cargo.toml | 1 + 17 files changed, 184 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e697ddff5..6b603e244 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2733,6 +2733,7 @@ dependencies = [ "ironrdp-rdpsnd", "ironrdp-svc", "ironrdp-tokio", + "qoicoubeh", "rayon", "rustls-pemfile", "tokio", @@ -2754,6 +2755,7 @@ dependencies = [ "ironrdp-graphics", "ironrdp-pdu", "ironrdp-svc", + "qoicoubeh", "tracing", ] @@ -4170,6 +4172,15 @@ dependencies = [ "unarray", ] +[[package]] +name = "qoicoubeh" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b82aa3fef8a980075775b8c46f874823b5b4a15de327d2dbb3b6fd818480ba" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-error" version = "1.2.3" diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 8124d3372..c2cbf9665 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -9,6 +9,10 @@ edition.workspace = true name = "perfenc" path = "src/perfenc.rs" +[features] +default = ["qoi"] +qoi = ["ironrdp/qoi"] + [dependencies] anyhow = "1.0.98" async-trait = "0.1.88" diff --git a/benches/src/perfenc.rs b/benches/src/perfenc.rs index 3f5a302f7..6b57560c8 100644 --- a/benches/src/perfenc.rs +++ b/benches/src/perfenc.rs @@ -28,7 +28,7 @@ async fn main() -> Result<(), anyhow::Error> { println!(" --width Width of the display (default: 3840)"); println!(" --height Height of the display (default: 2400)"); println!(" --codec Codec to use (default: remotefx)"); - println!(" Valid values: remotefx, bitmap, none"); + println!(" Valid values: qoi, remotefx, bitmap, none"); println!(" --fps Frames per second (default: none)"); std::process::exit(0); } @@ -52,6 +52,8 @@ async fn main() -> Result<(), anyhow::Error> { flags -= CmdFlags::SET_SURFACE_BITS; } OptCodec::None => {} + #[cfg(feature = "qoi")] + OptCodec::Qoi => update_codecs.set_qoi(Some(0)), }; let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs); @@ -172,6 +174,8 @@ enum OptCodec { RemoteFX, Bitmap, None, + #[cfg(feature = "qoi")] + Qoi, } impl Default for OptCodec { @@ -188,6 +192,8 @@ impl core::str::FromStr for OptCodec { "remotefx" => Ok(Self::RemoteFX), "bitmap" => Ok(Self::Bitmap), "none" => Ok(Self::None), + #[cfg(feature = "qoi")] + "qoi" => Ok(Self::Qoi), _ => Err(anyhow::anyhow!("unknown codec: {}", s)), } } diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index a9cfc9402..a09adfe32 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -27,6 +27,7 @@ test = false default = ["rustls"] rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"] native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"] +qoi = ["ironrdp/qoi"] [dependencies] # Protocols diff --git a/crates/ironrdp-connector/Cargo.toml b/crates/ironrdp-connector/Cargo.toml index b74ee82b3..0bcaac1c7 100644 --- a/crates/ironrdp-connector/Cargo.toml +++ b/crates/ironrdp-connector/Cargo.toml @@ -16,7 +16,9 @@ doctest = false test = false [features] +default = [] arbitrary = ["dep:arbitrary"] +qoi = ["ironrdp-pdu/qoi"] [dependencies] ironrdp-svc = { path = "../ironrdp-svc", version = "0.4" } # public diff --git a/crates/ironrdp-pdu/Cargo.toml b/crates/ironrdp-pdu/Cargo.toml index 4d424db56..132c1757e 100644 --- a/crates/ironrdp-pdu/Cargo.toml +++ b/crates/ironrdp-pdu/Cargo.toml @@ -19,6 +19,7 @@ doctest = false default = [] std = ["alloc", "ironrdp-error/std", "ironrdp-core/std"] alloc = ["ironrdp-core/alloc", "ironrdp-error/alloc"] +qoi = [] [dependencies] bitflags = "2.9" diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets.rs b/crates/ironrdp-pdu/src/rdp/capability_sets.rs index 6dd189a01..cad2a83f6 100644 --- a/crates/ironrdp-pdu/src/rdp/capability_sets.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets.rs @@ -34,7 +34,7 @@ pub use self::bitmap_cache::{ pub use self::bitmap_codecs::{ client_codecs_capabilities, server_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty, EntropyBits, Guid, NsCodec, RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags, - CODEC_ID_NONE, CODEC_ID_REMOTEFX, + CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_REMOTEFX, }; pub use self::brush::{Brush, SupportLevel}; pub use self::frame_acknowledge::FrameAcknowledge; diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs index 01a7a0eeb..807970ad7 100644 --- a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs @@ -40,6 +40,9 @@ const GUID_REMOTEFX: Guid = Guid(0x7677_2f12, 0xbd72, 0x4463, 0xaf, 0xb3, 0xb7, const GUID_IMAGE_REMOTEFX: Guid = Guid(0x2744_ccd4, 0x9d8a, 0x4e74, 0x80, 0x3c, 0x0e, 0xcb, 0xee, 0xa1, 0x9c, 0x54); #[rustfmt::skip] const GUID_IGNORE: Guid = Guid(0x9c43_51a6, 0x3535, 0x42ae, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, 0x58); +#[rustfmt::skip] +#[cfg(feature="qoi")] +const GUID_QOI: Guid = Guid(0x4dae_9af8, 0xb399, 0x4df6, 0xb4, 0x3a, 0x66, 0x2f, 0xd9, 0xc0, 0xf5, 0xd6); #[derive(Debug, PartialEq, Eq)] pub struct Guid(u32, u16, u16, u8, u8, u8, u8, u8, u8, u8, u8); @@ -167,6 +170,8 @@ impl Encode for Codec { CodecProperty::RemoteFx(_) => GUID_REMOTEFX, CodecProperty::ImageRemoteFx(_) => GUID_IMAGE_REMOTEFX, CodecProperty::Ignore => GUID_IGNORE, + #[cfg(feature = "qoi")] + CodecProperty::Qoi => GUID_QOI, _ => return Err(other_err!("invalid codec")), }; guid.encode(dst)?; @@ -204,6 +209,8 @@ impl Encode for Codec { } }; } + #[cfg(feature = "qoi")] + CodecProperty::Qoi => dst.write_u16(0), CodecProperty::Ignore => dst.write_u16(0), CodecProperty::None => dst.write_u16(0), }; @@ -227,6 +234,8 @@ impl Encode for Codec { RemoteFxContainer::ClientContainer(container) => container.size(), RemoteFxContainer::ServerContainer(size) => *size, }, + #[cfg(feature = "qoi")] + CodecProperty::Qoi => 0, CodecProperty::Ignore => 0, CodecProperty::None => 0, } @@ -264,6 +273,13 @@ impl<'de> Decode<'de> for Codec { } } GUID_IGNORE => CodecProperty::Ignore, + #[cfg(feature = "qoi")] + GUID_QOI => { + if !property_buffer.is_empty() { + return Err(invalid_field_err!("qoi property", "must be empty")); + } + CodecProperty::Qoi + } _ => CodecProperty::None, }; @@ -283,6 +299,8 @@ pub enum CodecProperty { RemoteFx(RemoteFxContainer), ImageRemoteFx(RemoteFxContainer), Ignore, + #[cfg(feature = "qoi")] + Qoi, None, } @@ -620,12 +638,14 @@ pub struct CodecId(u8); pub const CODEC_ID_NONE: CodecId = CodecId(0); pub const CODEC_ID_REMOTEFX: CodecId = CodecId(3); +pub const CODEC_ID_QOI: CodecId = CodecId(0x0A); impl Debug for CodecId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match self.0 { 0 => "None", 3 => "RemoteFx", + 0x0A => "QOI", _ => "unknown", }; write!(f, "CodecId({name})") @@ -637,6 +657,7 @@ impl CodecId { match value { 0 => Some(CODEC_ID_NONE), 3 => Some(CODEC_ID_REMOTEFX), + 0x0A => Some(CODEC_ID_QOI), _ => None, } } @@ -678,6 +699,7 @@ fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result Result>().join(", "); if !codec_names.is_empty() { return Err(format!("Unknown codecs: {codec_names}")); @@ -728,6 +759,7 @@ List of codecs: /// # List of codecs /// /// * `remotefx` (on by default) +/// * `qoi` (on by default, when feature "qoi") /// /// # Returns /// @@ -738,6 +770,7 @@ pub fn server_codecs_capabilities(config: &[&str]) -> Result>().join(", "); if !codec_names.is_empty() { return Err(format!("Unknown codecs: {codec_names}")); diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml index 4910bc44f..ec58b8691 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -16,9 +16,10 @@ doctest = true test = false [features] -default = ["rayon"] +default = ["rayon", "qoi"] helper = ["dep:x509-cert", "dep:rustls-pemfile"] rayon = ["dep:rayon"] +qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at any time. @@ -47,6 +48,7 @@ rustls-pemfile = { version = "2.2.0", optional = true } rayon = { version = "1.10.0", optional = true } bytes = "1" visibility = { version = "0.1", optional = true } +qoicoubeh = { version = "0.5", optional = true } [dev-dependencies] tokio = { version = "1", features = ["sync"] } diff --git a/crates/ironrdp-server/src/encoder/mod.rs b/crates/ironrdp-server/src/encoder/mod.rs index 1bd102add..8388f5b44 100644 --- a/crates/ironrdp-server/src/encoder/mod.rs +++ b/crates/ironrdp-server/src/encoder/mod.rs @@ -33,18 +33,30 @@ enum CodecId { #[derive(Debug)] pub(crate) struct UpdateEncoderCodecs { remotefx: Option<(EntropyBits, u8)>, + #[cfg(feature = "qoi")] + qoi: Option, } impl UpdateEncoderCodecs { #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) fn new() -> Self { - Self { remotefx: None } + Self { + remotefx: None, + #[cfg(feature = "qoi")] + qoi: None, + } } #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) fn set_remotefx(&mut self, remotefx: Option<(EntropyBits, u8)>) { self.remotefx = remotefx } + + #[cfg(feature = "qoi")] + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn set_qoi(&mut self, qoi: Option) { + self.qoi = qoi + } } impl Default for UpdateEncoderCodecs { @@ -78,6 +90,11 @@ impl UpdateEncoder { bitmap = BitmapUpdater::RemoteFx(RemoteFxHandler::new(algo, id, desktop_size)); } + #[cfg(feature = "qoi")] + if let Some(id) = codecs.qoi { + bitmap = BitmapUpdater::Qoi(QoiHandler::new(id)); + } + bitmap } else { BitmapUpdater::Bitmap(BitmapHandler::new()) @@ -287,6 +304,8 @@ enum BitmapUpdater { None(NoneHandler), Bitmap(BitmapHandler), RemoteFx(RemoteFxHandler), + #[cfg(feature = "qoi")] + Qoi(QoiHandler), } impl BitmapUpdater { @@ -295,6 +314,8 @@ impl BitmapUpdater { Self::None(up) => up.handle(bitmap), Self::Bitmap(up) => up.handle(bitmap), Self::RemoteFx(up) => up.handle(bitmap), + #[cfg(feature = "qoi")] + Self::Qoi(up) => up.handle(bitmap), } } @@ -408,6 +429,44 @@ impl BitmapUpdateHandler for RemoteFxHandler { } } +#[cfg(feature = "qoi")] +#[derive(Clone, Debug)] +struct QoiHandler { + codec_id: u8, +} + +#[cfg(feature = "qoi")] +impl QoiHandler { + fn new(codec_id: u8) -> Self { + Self { codec_id } + } +} + +#[cfg(feature = "qoi")] +impl BitmapUpdateHandler for QoiHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + use ironrdp_graphics::image_processing::PixelFormat::*; + + let raw_channels = match bitmap.format { + ARgb32 => qoi::RawChannels::Argb, + XRgb32 => qoi::RawChannels::Xrgb, + ABgr32 => qoi::RawChannels::Abgr, + XBgr32 => qoi::RawChannels::Xbgr, + BgrA32 => qoi::RawChannels::Bgra, + BgrX32 => qoi::RawChannels::Bgrx, + RgbA32 => qoi::RawChannels::Rgba, + RgbX32 => qoi::RawChannels::Rgbx, + }; + + let enc = qoi::EncoderBuilder::new(&bitmap.data, bitmap.width.get().into(), bitmap.height.get().into()) + .stride(bitmap.stride) + .raw_channels(raw_channels) + .build()?; + let data = enc.encode_to_vec()?; + set_surface(bitmap, self.codec_id, &data) + } +} + fn set_surface(bitmap: &BitmapUpdate, codec_id: u8, data: &[u8]) -> Result { let destination = ExclusiveRectangle { left: bitmap.x, diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index 0acb7603b..9e43c5371 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -55,6 +55,14 @@ impl RdpServerOptions { .iter() .any(|codec| matches!(codec.property, CodecProperty::RemoteFx(_))) } + + #[cfg(feature = "qoi")] + fn has_qoi(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::Qoi)) + } } #[derive(Clone)] @@ -742,6 +750,10 @@ impl RdpServer { } } CodecProperty::NsCodec(_) => (), + #[cfg(feature = "qoi")] + CodecProperty::Qoi if self.opts.has_qoi() => { + update_codecs.set_qoi(Some(codec.id)); + } _ => (), } } diff --git a/crates/ironrdp-session/Cargo.toml b/crates/ironrdp-session/Cargo.toml index cd7682042..93eee93d1 100644 --- a/crates/ironrdp-session/Cargo.toml +++ b/crates/ironrdp-session/Cargo.toml @@ -15,6 +15,10 @@ categories.workspace = true doctest = false test = false +[features] +default = [] +qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] + [dependencies] ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public ironrdp-connector = { path = "../ironrdp-connector", version = "0.6" } # public # TODO: at some point, this dependency could be removed (good for compilation speed) @@ -25,7 +29,7 @@ ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.4" } # public ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.5", features = ["std"] } # public ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.3" } tracing = { version = "0.1", features = ["log"] } +qoicoubeh = { version = "0.5", optional = true } [lints] workspace = true - diff --git a/crates/ironrdp-session/src/fast_path.rs b/crates/ironrdp-session/src/fast_path.rs index 59db1326b..db34614ed 100644 --- a/crates/ironrdp-session/src/fast_path.rs +++ b/crates/ironrdp-session/src/fast_path.rs @@ -361,6 +361,23 @@ impl Processor { .or(Some(rectangle)); } } + #[cfg(feature = "qoi")] + ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => { + let (header, decoded) = qoi::decode_to_vec(bits.extended_bitmap_data.data) + .map_err(|e| reason_err!("QOI decode", "{}", e))?; + match header.channels { + qoi::Channels::Rgb => { + let rectangle = image.apply_rgb24(&decoded, &destination, false)?; + + update_rectangle = update_rectangle + .map(|rect: InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + qoi::Channels::Rgba => { + warn!("Unsupported RGBA QOI data"); + } + } + } _ => { warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id); } diff --git a/crates/ironrdp-testsuite-core/Cargo.toml b/crates/ironrdp-testsuite-core/Cargo.toml index 5c1f3fe96..96b033934 100644 --- a/crates/ironrdp-testsuite-core/Cargo.toml +++ b/crates/ironrdp-testsuite-core/Cargo.toml @@ -44,7 +44,7 @@ ironrdp-graphics.path = "../ironrdp-graphics" ironrdp-input.path = "../ironrdp-input" ironrdp-rdcleanpath.path = "../ironrdp-rdcleanpath" ironrdp-rdpsnd.path = "../ironrdp-rdpsnd" -ironrdp-session.path = "../ironrdp-session" +ironrdp-session = { path = "../ironrdp-session", features = ["qoi"] } ironrdp-propertyset.path = "../ironrdp-propertyset" ironrdp-rdpfile.path = "../ironrdp-rdpfile" png = "0.17" diff --git a/crates/ironrdp-testsuite-core/tests/session/mod.rs b/crates/ironrdp-testsuite-core/tests/session/mod.rs index fd03457bf..dda2a99f1 100644 --- a/crates/ironrdp-testsuite-core/tests/session/mod.rs +++ b/crates/ironrdp-testsuite-core/tests/session/mod.rs @@ -14,11 +14,23 @@ mod tests { let config = &["remotefx:on"]; let capabilities = client_codecs_capabilities(config).unwrap(); - assert_eq!(capabilities.0.len(), 1); - assert!(matches!(capabilities.0[0].property, CodecProperty::RemoteFx(_))); + assert!(capabilities + .0 + .iter() + .any(|cap| matches!(cap.property, CodecProperty::RemoteFx(_)))); let config = &["remotefx:off"]; let capabilities = client_codecs_capabilities(config).unwrap(); - assert_eq!(capabilities.0.len(), 0); + assert!(!capabilities + .0 + .iter() + .any(|cap| matches!(cap.property, CodecProperty::RemoteFx(_)))); + + let config = &["qoi:on"]; + let capabilities = client_codecs_capabilities(config).unwrap(); + assert!(capabilities + .0 + .iter() + .any(|cap| matches!(cap.property, CodecProperty::Qoi))); } } diff --git a/crates/ironrdp-web/Cargo.toml b/crates/ironrdp-web/Cargo.toml index 286001a06..26186bac8 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -20,6 +20,7 @@ crate-type = ["cdylib", "rlib"] [features] default = ["panic_hook"] panic_hook = ["iron-remote-desktop/panic_hook"] +qoi = ["ironrdp/qoi"] [dependencies] # Protocols @@ -31,7 +32,7 @@ ironrdp = { path = "../ironrdp", features = [ "dvc", "cliprdr", "svc", - "displaycontrol" + "displaycontrol", ] } ironrdp-core.path = "../ironrdp-core" ironrdp-cliprdr-format.path = "../ironrdp-cliprdr-format" diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index cdfcb1178..888efbb7d 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -31,6 +31,7 @@ dvc = ["dep:ironrdp-dvc"] rdpdr = ["dep:ironrdp-rdpdr"] rdpsnd = ["dep:ironrdp-rdpsnd"] displaycontrol = ["dep:ironrdp-displaycontrol"] +qoi = ["ironrdp-server?/qoi", "ironrdp-pdu?/qoi", "ironrdp-connector?/qoi", "ironrdp-session?/qoi"] # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at any time. __bench = ["ironrdp-server/__bench"] From d2e56e8fdbc11e4d393c44f90bbbcea82cbab9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= Date: Wed, 5 Mar 2025 15:16:31 +0400 Subject: [PATCH 5/5] feat: add QOIZ image codec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new QOIZ codec (UUID 229cc6dc-a860-4b52-b4d8-053a22b3892b) for SetSurface command. The PDU data contains the same data as the QOI codec, with zstd compression. Some benchmarks showing interesting results (using ironrdp/perfenc) QOI: 10s user CPU, 96.20% compression QOIZ: 11s user CPU, 99.76% compression Signed-off-by: Marc-André Lureau --- Cargo.lock | 21 ++++ benches/Cargo.toml | 3 +- benches/src/perfenc.rs | 8 +- crates/ironrdp-client/Cargo.toml | 1 + crates/ironrdp-connector/Cargo.toml | 1 + crates/ironrdp-pdu/Cargo.toml | 1 + crates/ironrdp-pdu/src/rdp/capability_sets.rs | 2 +- .../src/rdp/capability_sets/bitmap_codecs.rs | 41 +++++++ crates/ironrdp-server/Cargo.toml | 4 +- crates/ironrdp-server/src/encoder/mod.rs | 114 +++++++++++++++--- crates/ironrdp-server/src/server.rs | 12 ++ crates/ironrdp-session/Cargo.toml | 2 + crates/ironrdp-session/src/fast_path.rs | 66 ++++++++-- crates/ironrdp-web/Cargo.toml | 1 + crates/ironrdp/Cargo.toml | 1 + 15 files changed, 243 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b603e244..4725922b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2741,6 +2741,7 @@ dependencies = [ "tracing", "visibility", "x509-cert", + "zstd-safe", ] [[package]] @@ -2757,6 +2758,7 @@ dependencies = [ "ironrdp-svc", "qoicoubeh", "tracing", + "zstd-safe", ] [[package]] @@ -6969,3 +6971,22 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/benches/Cargo.toml b/benches/Cargo.toml index c2cbf9665..17bcd94bb 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -10,8 +10,9 @@ name = "perfenc" path = "src/perfenc.rs" [features] -default = ["qoi"] +default = ["qoi", "qoiz"] qoi = ["ironrdp/qoi"] +qoiz = ["ironrdp/qoiz"] [dependencies] anyhow = "1.0.98" diff --git a/benches/src/perfenc.rs b/benches/src/perfenc.rs index 6b57560c8..b14dc5bc8 100644 --- a/benches/src/perfenc.rs +++ b/benches/src/perfenc.rs @@ -28,7 +28,7 @@ async fn main() -> Result<(), anyhow::Error> { println!(" --width Width of the display (default: 3840)"); println!(" --height Height of the display (default: 2400)"); println!(" --codec Codec to use (default: remotefx)"); - println!(" Valid values: qoi, remotefx, bitmap, none"); + println!(" Valid values: qoi, qoiz, remotefx, bitmap, none"); println!(" --fps Frames per second (default: none)"); std::process::exit(0); } @@ -54,6 +54,8 @@ async fn main() -> Result<(), anyhow::Error> { OptCodec::None => {} #[cfg(feature = "qoi")] OptCodec::Qoi => update_codecs.set_qoi(Some(0)), + #[cfg(feature = "qoiz")] + OptCodec::QoiZ => update_codecs.set_qoiz(Some(0)), }; let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs); @@ -176,6 +178,8 @@ enum OptCodec { None, #[cfg(feature = "qoi")] Qoi, + #[cfg(feature = "qoiz")] + QoiZ, } impl Default for OptCodec { @@ -194,6 +198,8 @@ impl core::str::FromStr for OptCodec { "none" => Ok(Self::None), #[cfg(feature = "qoi")] "qoi" => Ok(Self::Qoi), + #[cfg(feature = "qoiz")] + "qoiz" => Ok(Self::QoiZ), _ => Err(anyhow::anyhow!("unknown codec: {}", s)), } } diff --git a/crates/ironrdp-client/Cargo.toml b/crates/ironrdp-client/Cargo.toml index a09adfe32..2dca9052f 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -28,6 +28,7 @@ default = ["rustls"] rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"] native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"] qoi = ["ironrdp/qoi"] +qoiz = ["ironrdp/qoiz"] [dependencies] # Protocols diff --git a/crates/ironrdp-connector/Cargo.toml b/crates/ironrdp-connector/Cargo.toml index 0bcaac1c7..7d21a4e5c 100644 --- a/crates/ironrdp-connector/Cargo.toml +++ b/crates/ironrdp-connector/Cargo.toml @@ -19,6 +19,7 @@ test = false default = [] arbitrary = ["dep:arbitrary"] qoi = ["ironrdp-pdu/qoi"] +qoiz = ["ironrdp-pdu/qoiz"] [dependencies] ironrdp-svc = { path = "../ironrdp-svc", version = "0.4" } # public diff --git a/crates/ironrdp-pdu/Cargo.toml b/crates/ironrdp-pdu/Cargo.toml index 132c1757e..85f02fc27 100644 --- a/crates/ironrdp-pdu/Cargo.toml +++ b/crates/ironrdp-pdu/Cargo.toml @@ -20,6 +20,7 @@ default = [] std = ["alloc", "ironrdp-error/std", "ironrdp-core/std"] alloc = ["ironrdp-core/alloc", "ironrdp-error/alloc"] qoi = [] +qoiz = ["qoi"] [dependencies] bitflags = "2.9" diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets.rs b/crates/ironrdp-pdu/src/rdp/capability_sets.rs index cad2a83f6..8419f6976 100644 --- a/crates/ironrdp-pdu/src/rdp/capability_sets.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets.rs @@ -34,7 +34,7 @@ pub use self::bitmap_cache::{ pub use self::bitmap_codecs::{ client_codecs_capabilities, server_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty, EntropyBits, Guid, NsCodec, RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags, - CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_REMOTEFX, + CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_QOIZ, CODEC_ID_REMOTEFX, }; pub use self::brush::{Brush, SupportLevel}; pub use self::frame_acknowledge::FrameAcknowledge; diff --git a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs index 807970ad7..720d00bf7 100644 --- a/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs +++ b/crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs @@ -43,6 +43,9 @@ const GUID_IGNORE: Guid = Guid(0x9c43_51a6, 0x3535, 0x42ae, 0x91, 0x0c, 0xcd, 0x #[rustfmt::skip] #[cfg(feature="qoi")] const GUID_QOI: Guid = Guid(0x4dae_9af8, 0xb399, 0x4df6, 0xb4, 0x3a, 0x66, 0x2f, 0xd9, 0xc0, 0xf5, 0xd6); +#[rustfmt::skip] +#[cfg(feature="qoiz")] +const GUID_QOIZ: Guid = Guid(0x229c_c6dc, 0xa860, 0x4b52, 0xb4, 0xd8, 0x05, 0x3a, 0x22, 0xb3, 0x89, 0x2b); #[derive(Debug, PartialEq, Eq)] pub struct Guid(u32, u16, u16, u8, u8, u8, u8, u8, u8, u8, u8); @@ -172,6 +175,8 @@ impl Encode for Codec { CodecProperty::Ignore => GUID_IGNORE, #[cfg(feature = "qoi")] CodecProperty::Qoi => GUID_QOI, + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ => GUID_QOIZ, _ => return Err(other_err!("invalid codec")), }; guid.encode(dst)?; @@ -211,6 +216,8 @@ impl Encode for Codec { } #[cfg(feature = "qoi")] CodecProperty::Qoi => dst.write_u16(0), + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ => dst.write_u16(0), CodecProperty::Ignore => dst.write_u16(0), CodecProperty::None => dst.write_u16(0), }; @@ -236,6 +243,8 @@ impl Encode for Codec { }, #[cfg(feature = "qoi")] CodecProperty::Qoi => 0, + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ => 0, CodecProperty::Ignore => 0, CodecProperty::None => 0, } @@ -280,6 +289,13 @@ impl<'de> Decode<'de> for Codec { } CodecProperty::Qoi } + #[cfg(feature = "qoiz")] + GUID_QOIZ => { + if !property_buffer.is_empty() { + return Err(invalid_field_err!("qoi property", "must be empty")); + } + CodecProperty::QoiZ + } _ => CodecProperty::None, }; @@ -301,6 +317,8 @@ pub enum CodecProperty { Ignore, #[cfg(feature = "qoi")] Qoi, + #[cfg(feature = "qoiz")] + QoiZ, None, } @@ -639,6 +657,7 @@ pub struct CodecId(u8); pub const CODEC_ID_NONE: CodecId = CodecId(0); pub const CODEC_ID_REMOTEFX: CodecId = CodecId(3); pub const CODEC_ID_QOI: CodecId = CodecId(0x0A); +pub const CODEC_ID_QOIZ: CodecId = CodecId(0x0B); impl Debug for CodecId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -646,6 +665,7 @@ impl Debug for CodecId { 0 => "None", 3 => "RemoteFx", 0x0A => "QOI", + 0x0B => "QOIZ", _ => "unknown", }; write!(f, "CodecId({name})") @@ -658,6 +678,7 @@ impl CodecId { 0 => Some(CODEC_ID_NONE), 3 => Some(CODEC_ID_REMOTEFX), 0x0A => Some(CODEC_ID_QOI), + 0x0B => Some(CODEC_ID_QOIZ), _ => None, } } @@ -700,6 +721,7 @@ fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result Result>().join(", "); if !codec_names.is_empty() { return Err(format!("Unknown codecs: {codec_names}")); @@ -760,6 +791,7 @@ List of codecs: /// /// * `remotefx` (on by default) /// * `qoi` (on by default, when feature "qoi") +/// * `qoiz` (on by default, when feature "qoiz") /// /// # Returns /// @@ -771,6 +803,7 @@ pub fn server_codecs_capabilities(config: &[&str]) -> Result>().join(", "); if !codec_names.is_empty() { return Err(format!("Unknown codecs: {codec_names}")); diff --git a/crates/ironrdp-server/Cargo.toml b/crates/ironrdp-server/Cargo.toml index ec58b8691..2c1ca031c 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -16,10 +16,11 @@ doctest = true test = false [features] -default = ["rayon", "qoi"] +default = ["rayon", "qoi", "qoiz"] helper = ["dep:x509-cert", "dep:rustls-pemfile"] rayon = ["dep:rayon"] qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] +qoiz = ["dep:zstd-safe", "qoi", "ironrdp-pdu/qoiz"] # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at any time. @@ -49,6 +50,7 @@ rayon = { version = "1.10.0", optional = true } bytes = "1" visibility = { version = "0.1", optional = true } qoicoubeh = { version = "0.5", optional = true } +zstd-safe = { version = "7.2", optional = true } [dev-dependencies] tokio = { version = "1", features = ["sync"] } diff --git a/crates/ironrdp-server/src/encoder/mod.rs b/crates/ironrdp-server/src/encoder/mod.rs index 8388f5b44..66e871b35 100644 --- a/crates/ironrdp-server/src/encoder/mod.rs +++ b/crates/ironrdp-server/src/encoder/mod.rs @@ -1,7 +1,8 @@ use core::fmt; use core::num::NonZeroU16; +use std::sync::{Arc, Mutex}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use ironrdp_acceptor::DesktopSize; use ironrdp_graphics::diff::{find_different_rects_sub, Rect}; use ironrdp_pdu::encode_vec; @@ -35,6 +36,8 @@ pub(crate) struct UpdateEncoderCodecs { remotefx: Option<(EntropyBits, u8)>, #[cfg(feature = "qoi")] qoi: Option, + #[cfg(feature = "qoiz")] + qoiz: Option, } impl UpdateEncoderCodecs { @@ -44,6 +47,8 @@ impl UpdateEncoderCodecs { remotefx: None, #[cfg(feature = "qoi")] qoi: None, + #[cfg(feature = "qoiz")] + qoiz: None, } } @@ -57,6 +62,12 @@ impl UpdateEncoderCodecs { pub(crate) fn set_qoi(&mut self, qoi: Option) { self.qoi = qoi } + + #[cfg(feature = "qoiz")] + #[cfg_attr(feature = "__bench", visibility::make(pub))] + pub(crate) fn set_qoiz(&mut self, qoiz: Option) { + self.qoiz = qoiz + } } impl Default for UpdateEncoderCodecs { @@ -94,6 +105,10 @@ impl UpdateEncoder { if let Some(id) = codecs.qoi { bitmap = BitmapUpdater::Qoi(QoiHandler::new(id)); } + #[cfg(feature = "qoiz")] + if let Some(id) = codecs.qoiz { + bitmap = BitmapUpdater::Qoiz(QoizHandler::new(id)); + } bitmap } else { @@ -306,6 +321,8 @@ enum BitmapUpdater { RemoteFx(RemoteFxHandler), #[cfg(feature = "qoi")] Qoi(QoiHandler), + #[cfg(feature = "qoiz")] + Qoiz(QoizHandler), } impl BitmapUpdater { @@ -316,6 +333,8 @@ impl BitmapUpdater { Self::RemoteFx(up) => up.handle(bitmap), #[cfg(feature = "qoi")] Self::Qoi(up) => up.handle(bitmap), + #[cfg(feature = "qoiz")] + Self::Qoiz(up) => up.handle(bitmap), } } @@ -445,28 +464,85 @@ impl QoiHandler { #[cfg(feature = "qoi")] impl BitmapUpdateHandler for QoiHandler { fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { - use ironrdp_graphics::image_processing::PixelFormat::*; - - let raw_channels = match bitmap.format { - ARgb32 => qoi::RawChannels::Argb, - XRgb32 => qoi::RawChannels::Xrgb, - ABgr32 => qoi::RawChannels::Abgr, - XBgr32 => qoi::RawChannels::Xbgr, - BgrA32 => qoi::RawChannels::Bgra, - BgrX32 => qoi::RawChannels::Bgrx, - RgbA32 => qoi::RawChannels::Rgba, - RgbX32 => qoi::RawChannels::Rgbx, - }; - - let enc = qoi::EncoderBuilder::new(&bitmap.data, bitmap.width.get().into(), bitmap.height.get().into()) - .stride(bitmap.stride) - .raw_channels(raw_channels) - .build()?; - let data = enc.encode_to_vec()?; + let data = qoi_encode(bitmap)?; set_surface(bitmap, self.codec_id, &data) } } +#[cfg(feature = "qoiz")] +#[derive(Clone)] +struct QoizHandler { + codec_id: u8, + zctxt: Arc>>, +} + +#[cfg(feature = "qoiz")] +impl fmt::Debug for QoizHandler { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("QoizHandler").field("codec_id", &self.codec_id).finish() + } +} + +#[cfg(feature = "qoiz")] +impl QoizHandler { + fn new(codec_id: u8) -> Self { + let mut zctxt = zstd_safe::CCtx::default(); + + zctxt.set_parameter(zstd_safe::CParameter::CompressionLevel(3)).unwrap(); + zctxt + .set_parameter(zstd_safe::CParameter::EnableLongDistanceMatching(true)) + .unwrap(); + let zctxt = Arc::new(Mutex::new(zctxt)); + + Self { codec_id, zctxt } + } +} + +#[cfg(feature = "qoiz")] +impl BitmapUpdateHandler for QoizHandler { + fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { + let qoi = qoi_encode(bitmap)?; + let mut inb = zstd_safe::InBuffer::around(&qoi); + let mut data = vec![0; qoi.len()]; + let mut outb = zstd_safe::OutBuffer::around(data.as_mut_slice()); + + let mut zctxt = self.zctxt.lock().unwrap(); + let res = zctxt + .compress_stream2( + &mut outb, + &mut inb, + zstd_safe::zstd_sys::ZSTD_EndDirective::ZSTD_e_flush, + ) + .map_err(zstd_safe::get_error_name) + .unwrap(); + if res != 0 { + return Err(anyhow!("Failed to zstd compress")); + } + + set_surface(bitmap, self.codec_id, outb.as_slice()) + } +} + +#[cfg(feature = "qoi")] +fn qoi_encode(bitmap: &BitmapUpdate) -> Result> { + use ironrdp_graphics::image_processing::PixelFormat::*; + let raw_channels = match bitmap.format { + ARgb32 => qoi::RawChannels::Argb, + XRgb32 => qoi::RawChannels::Xrgb, + ABgr32 => qoi::RawChannels::Abgr, + XBgr32 => qoi::RawChannels::Xbgr, + BgrA32 => qoi::RawChannels::Bgra, + BgrX32 => qoi::RawChannels::Bgrx, + RgbA32 => qoi::RawChannels::Rgba, + RgbX32 => qoi::RawChannels::Rgbx, + }; + let enc = qoi::EncoderBuilder::new(&bitmap.data, bitmap.width.get().into(), bitmap.height.get().into()) + .stride(bitmap.stride) + .raw_channels(raw_channels) + .build()?; + Ok(enc.encode_to_vec()?) +} + fn set_surface(bitmap: &BitmapUpdate, codec_id: u8, data: &[u8]) -> Result { let destination = ExclusiveRectangle { left: bitmap.x, diff --git a/crates/ironrdp-server/src/server.rs b/crates/ironrdp-server/src/server.rs index 9e43c5371..54a8e79de 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -63,6 +63,14 @@ impl RdpServerOptions { .iter() .any(|codec| matches!(codec.property, CodecProperty::Qoi)) } + + #[cfg(feature = "qoiz")] + fn has_qoiz(&self) -> bool { + self.codecs + .0 + .iter() + .any(|codec| matches!(codec.property, CodecProperty::QoiZ)) + } } #[derive(Clone)] @@ -754,6 +762,10 @@ impl RdpServer { CodecProperty::Qoi if self.opts.has_qoi() => { update_codecs.set_qoi(Some(codec.id)); } + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ if self.opts.has_qoiz() => { + update_codecs.set_qoiz(Some(codec.id)); + } _ => (), } } diff --git a/crates/ironrdp-session/Cargo.toml b/crates/ironrdp-session/Cargo.toml index 93eee93d1..e498c3eaf 100644 --- a/crates/ironrdp-session/Cargo.toml +++ b/crates/ironrdp-session/Cargo.toml @@ -18,6 +18,7 @@ test = false [features] default = [] qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] +qoiz = ["dep:zstd-safe", "qoi"] [dependencies] ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public @@ -30,6 +31,7 @@ ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.5", features = ["std"] } # ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.3" } tracing = { version = "0.1", features = ["log"] } qoicoubeh = { version = "0.5", optional = true } +zstd-safe = { version = "7.2", optional = true, features = ["std"] } [lints] workspace = true diff --git a/crates/ironrdp-session/src/fast_path.rs b/crates/ironrdp-session/src/fast_path.rs index db34614ed..229108fd6 100644 --- a/crates/ironrdp-session/src/fast_path.rs +++ b/crates/ironrdp-session/src/fast_path.rs @@ -37,6 +37,8 @@ pub struct Processor { mouse_pos_update: Option<(u16, u16)>, no_server_pointer: bool, pointer_software_rendering: bool, + #[cfg(feature = "qoiz")] + zdctx: zstd_safe::DCtx<'static>, } impl Processor { @@ -363,20 +365,34 @@ impl Processor { } #[cfg(feature = "qoi")] ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => { - let (header, decoded) = qoi::decode_to_vec(bits.extended_bitmap_data.data) - .map_err(|e| reason_err!("QOI decode", "{}", e))?; - match header.channels { - qoi::Channels::Rgb => { - let rectangle = image.apply_rgb24(&decoded, &destination, false)?; - - update_rectangle = update_rectangle - .map(|rect: InclusiveRectangle| rect.union(&rectangle)) - .or(Some(rectangle)); - } - qoi::Channels::Rgba => { - warn!("Unsupported RGBA QOI data"); + qoi_apply( + image, + destination, + bits.extended_bitmap_data.data, + &mut update_rectangle, + )?; + } + #[cfg(feature = "qoiz")] + ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ => { + let compressed = &bits.extended_bitmap_data.data; + let mut input = zstd_safe::InBuffer::around(compressed); + let mut data = vec![0; compressed.len() * 4]; + let mut pos = 0; + loop { + let mut output = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos); + self.zdctx + .decompress_stream(&mut output, &mut input) + .map_err(zstd_safe::get_error_name) + .map_err(|e| reason_err!("zstd", "{}", e))?; + pos = output.pos(); + if pos == output.capacity() { + data.resize(data.capacity() * 2, 0); + } else { + break; } } + + qoi_apply(image, destination, &data, &mut update_rectangle)?; } _ => { warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id); @@ -398,6 +414,30 @@ impl Processor { } } +#[cfg(feature = "qoi")] +fn qoi_apply( + image: &mut DecodedImage, + destination: InclusiveRectangle, + data: &[u8], + update_rectangle: &mut Option, +) -> SessionResult<()> { + let (header, decoded) = qoi::decode_to_vec(data).map_err(|e| reason_err!("QOI decode", "{}", e))?; + match header.channels { + qoi::Channels::Rgb => { + let rectangle = image.apply_rgb24(&decoded, &destination, false)?; + + *update_rectangle = update_rectangle + .as_ref() + .map(|rect: &InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + qoi::Channels::Rgba => { + warn!("Unsupported RGBA QOI data"); + } + } + Ok(()) +} + pub struct ProcessorBuilder { pub io_channel_id: u16, pub user_channel_id: u16, @@ -421,6 +461,8 @@ impl ProcessorBuilder { mouse_pos_update: None, no_server_pointer: self.no_server_pointer, pointer_software_rendering: self.pointer_software_rendering, + #[cfg(feature = "qoiz")] + zdctx: zstd_safe::DCtx::default(), } } } diff --git a/crates/ironrdp-web/Cargo.toml b/crates/ironrdp-web/Cargo.toml index 26186bac8..7e25624f4 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -21,6 +21,7 @@ crate-type = ["cdylib", "rlib"] default = ["panic_hook"] panic_hook = ["iron-remote-desktop/panic_hook"] qoi = ["ironrdp/qoi"] +qoiz = ["ironrdp/qoiz"] [dependencies] # Protocols diff --git a/crates/ironrdp/Cargo.toml b/crates/ironrdp/Cargo.toml index 888efbb7d..e573c459a 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -32,6 +32,7 @@ rdpdr = ["dep:ironrdp-rdpdr"] rdpsnd = ["dep:ironrdp-rdpsnd"] displaycontrol = ["dep:ironrdp-displaycontrol"] qoi = ["ironrdp-server?/qoi", "ironrdp-pdu?/qoi", "ironrdp-connector?/qoi", "ironrdp-session?/qoi"] +qoiz = ["ironrdp-server?/qoiz", "ironrdp-pdu?/qoiz", "ironrdp-connector?/qoiz", "ironrdp-session?/qoiz"] # Internal (PRIVATE!) features used to aid testing. # Don't rely on these whatsoever. They may disappear at any time. __bench = ["ironrdp-server/__bench"]