From f75bf13d852e925683a0c9036c8cb94586207375 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 1/4] feat(session): add apply_rgb24() to apply non-inverted bitmap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marc-André Lureau --- crates/ironrdp-session/src/image.rs | 39 +++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/ironrdp-session/src/image.rs b/crates/ironrdp-session/src/image.rs index 9a5bed9c3..708ef2006 100644 --- a/crates/ironrdp-session/src/image.rs +++ b/crates/ironrdp-session/src/image.rs @@ -572,7 +572,7 @@ impl DecodedImage { } // FIXME: this assumes PixelFormat::RgbA32 - pub(crate) fn apply_rgb24_bitmap( + pub(crate) fn apply_rgb24( &mut self, rgb24: &[u8], update_rectangle: &InclusiveRectangle, @@ -587,28 +587,35 @@ impl DecodedImage { 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; + let it = rgb24.chunks_exact(rectangle_width * SRC_COLOR_DEPTH); + // can it monomorphize this? + let it: Box> = if REV { Box::new(it.rev()) } else { Box::new(it) }; + it.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_bitmap( + &mut self, + rgb24: &[u8], + update_rectangle: &InclusiveRectangle, + ) -> SessionResult { + self.apply_rgb24::(rgb24, update_rectangle) + } + // FIXME: this assumes PixelFormat::RgbA32 pub(crate) fn apply_rgb32_bitmap( &mut self, From 0cebbb50c0adc676f89ebc53277a68cc3c6286f6 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 2/4] feature(server)!: replace with_remote_fx option with codecs 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. 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 | 15 +-- crates/ironrdp-server/src/capabilities.rs | 19 +--- crates/ironrdp-server/src/server.rs | 33 ++++-- 5 files changed, 110 insertions(+), 63 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 07c28b325..d21b61c51 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. +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 241ab2e38..59bf08672 100644 --- a/crates/ironrdp-server/src/builder.rs +++ b/crates/ironrdp-server/src/builder.rs @@ -1,6 +1,7 @@ use std::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,9 +156,9 @@ impl RdpServerBuilder { self } - pub fn with_remote_fx(mut self, enabled: bool) -> Self { - self.state.with_remote_fx = enabled; - self + pub fn with_codecs_config(mut self, config: &[&str]) -> Result { + self.state.codecs = server_codecs_capabilities(config)?; + Ok(self) } pub fn build(self) -> RdpServer { @@ -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 49fa849d0..fcdedf867 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,22 @@ 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)] @@ -710,21 +725,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 cb58d859b38c9c123ba4433c63bfeab6ef07bda8 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 3/4] 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 [1]: https://qoiformat.org/ Signed-off-by: Marc-André Lureau --- .cargo/config.toml | 3 + Cargo.lock | 10 +++ 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 | 64 ++++++++++++++++++- crates/ironrdp-server/src/server.rs | 12 ++++ crates/ironrdp-session/Cargo.toml | 6 +- crates/ironrdp-session/src/fast_path.rs | 21 ++++++ crates/ironrdp-testsuite-core/Cargo.toml | 2 +- .../tests/session/mod.rs | 18 +++++- crates/ironrdp-web/Cargo.toml | 5 +- crates/ironrdp/Cargo.toml | 1 + fuzz/Cargo.lock | 5 ++ 19 files changed, 199 insertions(+), 11 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 35049cbcb..9b2a5bb11 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ [alias] xtask = "run --package xtask --" + +[patch.crates-io] +qoi = { git = "https://github.com/elmarco/qoi-rust.git", branch = "raw" } diff --git a/Cargo.lock b/Cargo.lock index 7e9a9c5e0..3e9e3f70f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2722,6 +2722,7 @@ dependencies = [ "ironrdp-rdpsnd", "ironrdp-svc", "ironrdp-tokio", + "qoi", "rayon", "rustls-pemfile", "tokio", @@ -2743,6 +2744,7 @@ dependencies = [ "ironrdp-graphics", "ironrdp-pdu", "ironrdp-svc", + "qoi", "tracing", ] @@ -4143,6 +4145,14 @@ dependencies = [ "unarray", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "git+https://github.com/elmarco/qoi-rust.git?branch=raw#9fc76e899f9421eb203930dd6a54600f1e6e4b1c" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-error" version = "1.2.3" diff --git a/benches/Cargo.toml b/benches/Cargo.toml index b5f69ca24..c0858e1e7 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 00e1427b1..0e6e8ebf1 100644 --- a/benches/src/perfenc.rs +++ b/benches/src/perfenc.rs @@ -27,7 +27,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); } @@ -51,6 +51,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); @@ -171,6 +173,8 @@ enum OptCodec { RemoteFX, Bitmap, None, + #[cfg(feature = "qoi")] + Qoi, } impl Default for OptCodec { @@ -187,6 +191,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 5e6eaef56..5629b7f5a 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 3e42081f7..53105313a 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.3" } # public diff --git a/crates/ironrdp-pdu/Cargo.toml b/crates/ironrdp-pdu/Cargo.toml index d2dbe0cc7..88ca6ac89 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.4" 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 d21b61c51..dc7540767 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)); @@ -729,6 +760,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 352b7214e..cf7ed6e55 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:qoi", "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 } +qoi = { version = "0.4", 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..7d12cd985 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,47 @@ 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 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::Encoder::new_raw( + &bitmap.data, + bitmap.width.get().into(), + bitmap.height.get().into(), + bitmap.stride, + channels, + )?; + 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 fcdedf867..5f1fb5b0c 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -54,6 +54,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)] @@ -740,6 +748,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 477c541ec..42c4bb69a 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:qoi", "ironrdp-pdu/qoi"] + [dependencies] ironrdp-core = { path = "../ironrdp-core", version = "0.1" } # public ironrdp-connector = { path = "../ironrdp-connector", version = "0.4" } # 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.3" } # public ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.4", features = ["std"] } # public ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.2" } tracing = { version = "0.1", features = ["log"] } +qoi = { version = "0.4", optional = true } [lints] workspace = true - diff --git a/crates/ironrdp-session/src/fast_path.rs b/crates/ironrdp-session/src/fast_path.rs index d0a25da0b..cd1a5547e 100644 --- a/crates/ironrdp-session/src/fast_path.rs +++ b/crates/ironrdp-session/src/fast_path.rs @@ -9,6 +9,8 @@ use ironrdp_pdu::codecs::rfx::FrameAcknowledgePdu; use ironrdp_pdu::fast_path::{FastPathHeader, FastPathUpdate, FastPathUpdatePdu, Fragmentation}; use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; use ironrdp_pdu::pointer::PointerUpdateData; +#[cfg(feature = "qoi")] +use ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI; use ironrdp_pdu::rdp::capability_sets::{CodecId, CODEC_ID_NONE, CODEC_ID_REMOTEFX}; use ironrdp_pdu::rdp::headers::ShareDataPdu; use ironrdp_pdu::surface_commands::{FrameAction, FrameMarkerPdu, SurfaceCommand}; @@ -361,6 +363,25 @@ impl Processor { .or(Some(rectangle)); } } + #[cfg(feature = "qoi")] + 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)?; + + update_rectangle = update_rectangle + .map(|rect: InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + qoi::Channels::Rgba => { + warn!("Unsupported RGBA QOI data"); + // TODO: bitmap is rev... + // image.apply_rgb32_bitmap(&decoded, PixelFormat::RgbA32, &destination)?; + } + } + } _ => { 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 0cc87ae57..dbeb14174 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"] } png = "0.17" pretty_assertions = "1.4" proptest.workspace = true 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 a2d0f9d26..d158fe6ec 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -18,8 +18,9 @@ test = false crate-type = ["cdylib", "rlib"] [features] -default = ["panic_hook"] +default = ["panic_hook", "qoi"] 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 3abbcb7d6..5e38b352e 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"] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index fad6b1e24..a5602702a 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -817,3 +817,8 @@ dependencies = [ "quote", "syn", ] + +[[patch.unused]] +name = "qoi" +version = "0.4.1" +source = "git+https://github.com/elmarco/qoi-rust.git?branch=raw#9fc76e899f9421eb203930dd6a54600f1e6e4b1c" From 39c30d90cd06b60e3a813d83c4b683f9a875077d 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 4/4] 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 | 9 +- 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 | 121 ++++++++++++++---- crates/ironrdp-server/src/server.rs | 13 ++ crates/ironrdp-session/Cargo.toml | 2 + crates/ironrdp-session/src/fast_path.rs | 72 +++++++++-- crates/ironrdp-web/Cargo.toml | 2 + crates/ironrdp/Cargo.toml | 1 + 15 files changed, 254 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e9e3f70f..5de4a6994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2730,6 +2730,7 @@ dependencies = [ "tracing", "visibility", "x509-cert", + "zstd-safe", ] [[package]] @@ -2746,6 +2747,7 @@ dependencies = [ "ironrdp-svc", "qoi", "tracing", + "zstd-safe", ] [[package]] @@ -6862,3 +6864,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 c0858e1e7..c9fda1dec 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 0e6e8ebf1..0a4977f0e 100644 --- a/benches/src/perfenc.rs +++ b/benches/src/perfenc.rs @@ -13,6 +13,7 @@ use ironrdp::server::{ }; use tokio::{fs::File, io::AsyncReadExt, time::sleep}; +#[allow(clippy::similar_names)] #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), anyhow::Error> { setup_logging()?; @@ -27,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); } @@ -53,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); @@ -175,6 +178,8 @@ enum OptCodec { None, #[cfg(feature = "qoi")] Qoi, + #[cfg(feature = "qoiz")] + QoiZ, } impl Default for OptCodec { @@ -193,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 5629b7f5a..6fb456d95 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 53105313a..80f8dd99d 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.3" } # public diff --git a/crates/ironrdp-pdu/Cargo.toml b/crates/ironrdp-pdu/Cargo.toml index 88ca6ac89..6f6698429 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.4" 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 dc7540767..c80b74ca9 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)); @@ -761,6 +792,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 cf7ed6e55..c6130cac1 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:qoi", "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 } qoi = { version = "0.4", 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 7d12cd985..1a2b2055d 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 { @@ -81,6 +92,7 @@ impl fmt::Debug for UpdateEncoder { } impl UpdateEncoder { + #[allow(clippy::similar_names)] #[cfg_attr(feature = "__bench", visibility::make(pub))] pub(crate) fn new(desktop_size: DesktopSize, surface_flags: CmdFlags, codecs: UpdateEncoderCodecs) -> Self { let bitmap_updater = if surface_flags.contains(CmdFlags::SET_SURFACE_BITS) { @@ -94,6 +106,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 +322,8 @@ enum BitmapUpdater { RemoteFx(RemoteFxHandler), #[cfg(feature = "qoi")] Qoi(QoiHandler), + #[cfg(feature = "qoiz")] + Qoiz(QoizHandler), } impl BitmapUpdater { @@ -316,6 +334,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,31 +465,88 @@ impl QoiHandler { #[cfg(feature = "qoi")] impl BitmapUpdateHandler for QoiHandler { fn handle(&mut self, bitmap: &BitmapUpdate) -> Result { - use ironrdp_graphics::image_processing::PixelFormat::*; - - let 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::Encoder::new_raw( - &bitmap.data, - bitmap.width.get().into(), - bitmap.height.get().into(), - bitmap.stride, - channels, - )?; - 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 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::Encoder::new_raw( + &bitmap.data, + bitmap.width.get().into(), + bitmap.height.get().into(), + bitmap.stride, + channels, + )?; + 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 5f1fb5b0c..5a79623c0 100644 --- a/crates/ironrdp-server/src/server.rs +++ b/crates/ironrdp-server/src/server.rs @@ -62,6 +62,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)] @@ -650,6 +658,7 @@ impl RdpServer { state } + #[allow(clippy::similar_names)] async fn client_accepted( &mut self, reader: &mut Framed, @@ -752,6 +761,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 42c4bb69a..46d89d2ed 100644 --- a/crates/ironrdp-session/Cargo.toml +++ b/crates/ironrdp-session/Cargo.toml @@ -18,6 +18,7 @@ test = false [features] default = [] qoi = ["dep:qoi", "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.4", features = ["std"] } # ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.2" } tracing = { version = "0.1", features = ["log"] } qoi = { version = "0.4", 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 cd1a5547e..b7079357e 100644 --- a/crates/ironrdp-session/src/fast_path.rs +++ b/crates/ironrdp-session/src/fast_path.rs @@ -11,6 +11,8 @@ use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; use ironrdp_pdu::pointer::PointerUpdateData; #[cfg(feature = "qoi")] use ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI; +#[cfg(feature = "qoiz")] +use ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ; use ironrdp_pdu::rdp::capability_sets::{CodecId, CODEC_ID_NONE, CODEC_ID_REMOTEFX}; use ironrdp_pdu::rdp::headers::ShareDataPdu; use ironrdp_pdu::surface_commands::{FrameAction, FrameMarkerPdu, SurfaceCommand}; @@ -39,6 +41,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 { @@ -365,22 +369,34 @@ impl Processor { } #[cfg(feature = "qoi")] 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)?; - - update_rectangle = update_rectangle - .map(|rect: InclusiveRectangle| rect.union(&rectangle)) - .or(Some(rectangle)); - } - qoi::Channels::Rgba => { - warn!("Unsupported RGBA QOI data"); - // TODO: bitmap is rev... - // image.apply_rgb32_bitmap(&decoded, PixelFormat::RgbA32, &destination)?; + qoi_apply( + image, + destination, + bits.extended_bitmap_data.data, + &mut update_rectangle, + )?; + } + #[cfg(feature = "qoiz")] + 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); @@ -402,6 +418,32 @@ 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)?; + + *update_rectangle = update_rectangle + .as_ref() + .map(|rect: &InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + qoi::Channels::Rgba => { + warn!("Unsupported RGBA QOI data"); + // TODO: bitmap is rev... + // image.apply_rgb32_bitmap(&decoded, PixelFormat::RgbA32, &destination)?; + } + } + Ok(()) +} + pub struct ProcessorBuilder { pub io_channel_id: u16, pub user_channel_id: u16, @@ -425,6 +467,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 d158fe6ec..312820455 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -18,9 +18,11 @@ test = false crate-type = ["cdylib", "rlib"] [features] +# qoiz is not enabled by default, because MacOS github runner is lacking clang wasm target default = ["panic_hook", "qoi"] 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 5e38b352e..11426e66d 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"]