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..5de4a6994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2722,6 +2722,7 @@ dependencies = [ "ironrdp-rdpsnd", "ironrdp-svc", "ironrdp-tokio", + "qoi", "rayon", "rustls-pemfile", "tokio", @@ -2729,6 +2730,7 @@ dependencies = [ "tracing", "visibility", "x509-cert", + "zstd-safe", ] [[package]] @@ -2743,7 +2745,9 @@ dependencies = [ "ironrdp-graphics", "ironrdp-pdu", "ironrdp-svc", + "qoi", "tracing", + "zstd-safe", ] [[package]] @@ -4143,6 +4147,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" @@ -6852,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 b5f69ca24..c9fda1dec 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -9,6 +9,11 @@ edition.workspace = true name = "perfenc" path = "src/perfenc.rs" +[features] +default = ["qoi", "qoiz"] +qoi = ["ironrdp/qoi"] +qoiz = ["ironrdp/qoiz"] + [dependencies] anyhow = "1.0.98" async-trait = "0.1.88" diff --git a/benches/src/perfenc.rs b/benches/src/perfenc.rs index 00e1427b1..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: remotefx, bitmap, none"); + println!(" Valid values: qoi, qoiz, remotefx, bitmap, none"); println!(" --fps Frames per second (default: none)"); std::process::exit(0); } @@ -51,6 +52,10 @@ async fn main() -> Result<(), anyhow::Error> { flags -= CmdFlags::SET_SURFACE_BITS; } 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); @@ -171,6 +176,10 @@ enum OptCodec { RemoteFX, Bitmap, None, + #[cfg(feature = "qoi")] + Qoi, + #[cfg(feature = "qoiz")] + QoiZ, } impl Default for OptCodec { @@ -187,6 +196,10 @@ 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), + #[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 5e6eaef56..6fb456d95 100644 --- a/crates/ironrdp-client/Cargo.toml +++ b/crates/ironrdp-client/Cargo.toml @@ -27,6 +27,8 @@ 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"] +qoiz = ["ironrdp/qoiz"] [dependencies] # Protocols diff --git a/crates/ironrdp-connector/Cargo.toml b/crates/ironrdp-connector/Cargo.toml index 3e42081f7..80f8dd99d 100644 --- a/crates/ironrdp-connector/Cargo.toml +++ b/crates/ironrdp-connector/Cargo.toml @@ -16,7 +16,10 @@ doctest = false test = false [features] +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 d2dbe0cc7..6f6698429 100644 --- a/crates/ironrdp-pdu/Cargo.toml +++ b/crates/ironrdp-pdu/Cargo.toml @@ -19,6 +19,8 @@ doctest = false 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 38bb5a39e..8419f6976 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_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 07c28b325..c80b74ca9 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::{ @@ -39,6 +40,12 @@ 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); +#[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); @@ -166,6 +173,10 @@ impl Encode for Codec { CodecProperty::RemoteFx(_) => GUID_REMOTEFX, CodecProperty::ImageRemoteFx(_) => GUID_IMAGE_REMOTEFX, 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)?; @@ -203,6 +214,10 @@ 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), }; @@ -226,6 +241,10 @@ impl Encode for Codec { RemoteFxContainer::ClientContainer(container) => container.size(), RemoteFxContainer::ServerContainer(size) => *size, }, + #[cfg(feature = "qoi")] + CodecProperty::Qoi => 0, + #[cfg(feature = "qoiz")] + CodecProperty::QoiZ => 0, CodecProperty::Ignore => 0, CodecProperty::None => 0, } @@ -263,6 +282,20 @@ 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 + } + #[cfg(feature = "qoiz")] + GUID_QOIZ => { + if !property_buffer.is_empty() { + return Err(invalid_field_err!("qoi property", "must be empty")); + } + CodecProperty::QoiZ + } _ => CodecProperty::None, }; @@ -282,6 +315,10 @@ pub enum CodecProperty { RemoteFx(RemoteFxContainer), ImageRemoteFx(RemoteFxContainer), Ignore, + #[cfg(feature = "qoi")] + Qoi, + #[cfg(feature = "qoiz")] + QoiZ, None, } @@ -619,12 +656,16 @@ 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 { let name = match self.0 { 0 => "None", 3 => "RemoteFx", + 0x0A => "QOI", + 0x0B => "QOIZ", _ => "unknown", }; write!(f, "CodecId({})", name) @@ -636,11 +677,37 @@ impl CodecId { match value { 0 => Some(CODEC_ID_NONE), 3 => Some(CODEC_ID_REMOTEFX), + 0x0A => Some(CODEC_ID_QOI), + 0x0B => Some(CODEC_ID_QOIZ), _ => None, } } } +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. /// @@ -653,45 +720,24 @@ impl CodecId { /// # List of codecs /// /// * `remotefx` (on by default) +/// * `qoi` (on by default, when feature "qoi") +/// * `qoiz` (on by default, when feature "qoiz") /// /// # Returns /// /// 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: - `remotefx` (on by default) +- `qoi` (on by default, when feature "qoi") +- `qoiz` (on by default, when feature "qoiz") "# .to_owned()); } + let mut config = parse_codecs_config(config)?; let mut codecs = vec![]; @@ -708,6 +754,90 @@ List of codecs: }); } + #[cfg(feature = "qoi")] + if config.remove("qoi").unwrap_or(true) { + codecs.push(Codec { + id: CODEC_ID_QOI.0, + property: CodecProperty::Qoi, + }); + } + + #[cfg(feature = "qoiz")] + if config.remove("qoiz").unwrap_or(true) { + codecs.push(Codec { + id: CODEC_ID_QOIZ.0, + property: CodecProperty::QoiZ, + }); + } + + let codec_names = config.keys().copied().collect::>().join(", "); + if !codec_names.is_empty() { + return Err(format!("Unknown codecs: {}", codec_names)); + } + + 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) +/// * `qoi` (on by default, when feature "qoi") +/// * `qoiz` (on by default, when feature "qoiz") +/// +/// # 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) +- `qoi` (on by default, when feature "qoi") +- `qoiz` (on by default, when feature "qoiz") +"# + .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)), + }); + } + + #[cfg(feature = "qoi")] + if config.remove("qoi").unwrap_or(true) { + codecs.push(Codec { + id: 0, + property: CodecProperty::Qoi, + }); + } + + #[cfg(feature = "qoiz")] + if config.remove("qoiz").unwrap_or(true) { + codecs.push(Codec { + id: 0, + property: CodecProperty::QoiZ, + }); + } + let codec_names = config.keys().copied().collect::>().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..c6130cac1 100644 --- a/crates/ironrdp-server/Cargo.toml +++ b/crates/ironrdp-server/Cargo.toml @@ -16,9 +16,11 @@ doctest = true test = false [features] -default = ["rayon"] +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. @@ -47,6 +49,8 @@ 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 } +zstd-safe = { version = "7.2", optional = true } [dev-dependencies] tokio = { version = "1", features = ["sync"] } 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/encoder/mod.rs b/crates/ironrdp-server/src/encoder/mod.rs index 1bd102add..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; @@ -33,18 +34,40 @@ enum CodecId { #[derive(Debug)] pub(crate) struct UpdateEncoderCodecs { remotefx: Option<(EntropyBits, u8)>, + #[cfg(feature = "qoi")] + qoi: Option, + #[cfg(feature = "qoiz")] + qoiz: 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(feature = "qoiz")] + qoiz: 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 + } + + #[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 { @@ -69,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) { @@ -78,6 +102,15 @@ 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)); + } + #[cfg(feature = "qoiz")] + if let Some(id) = codecs.qoiz { + bitmap = BitmapUpdater::Qoiz(QoizHandler::new(id)); + } + bitmap } else { BitmapUpdater::Bitmap(BitmapHandler::new()) @@ -287,6 +320,10 @@ enum BitmapUpdater { None(NoneHandler), Bitmap(BitmapHandler), RemoteFx(RemoteFxHandler), + #[cfg(feature = "qoi")] + Qoi(QoiHandler), + #[cfg(feature = "qoiz")] + Qoiz(QoizHandler), } impl BitmapUpdater { @@ -295,6 +332,10 @@ 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), + #[cfg(feature = "qoiz")] + Self::Qoiz(up) => up.handle(bitmap), } } @@ -408,6 +449,104 @@ 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 { + 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 49fa849d0..5a79623c0 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,38 @@ 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(_))) + } + + #[cfg(feature = "qoi")] + fn has_qoi(&self) -> bool { + self.codecs + .0 + .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)] @@ -627,6 +658,7 @@ impl RdpServer { state } + #[allow(clippy::similar_names)] async fn client_accepted( &mut self, reader: &mut Framed, @@ -710,21 +742,29 @@ 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(_) => (), + #[cfg(feature = "qoi")] + 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 477c541ec..46d89d2ed 100644 --- a/crates/ironrdp-session/Cargo.toml +++ b/crates/ironrdp-session/Cargo.toml @@ -15,6 +15,11 @@ categories.workspace = true doctest = false 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 ironrdp-connector = { path = "../ironrdp-connector", version = "0.4" } # public # TODO: at some point, this dependency could be removed (good for compilation speed) @@ -25,7 +30,8 @@ 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 } +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 d0a25da0b..b7079357e 100644 --- a/crates/ironrdp-session/src/fast_path.rs +++ b/crates/ironrdp-session/src/fast_path.rs @@ -9,6 +9,10 @@ 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; +#[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}; @@ -37,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 { @@ -361,6 +367,37 @@ impl Processor { .or(Some(rectangle)); } } + #[cfg(feature = "qoi")] + CODEC_ID_QOI => { + 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); } @@ -381,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, @@ -404,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-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, 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..312820455 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -18,8 +18,11 @@ test = false crate-type = ["cdylib", "rlib"] [features] -default = ["panic_hook"] +# 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 @@ -31,7 +34,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..11426e66d 100644 --- a/crates/ironrdp/Cargo.toml +++ b/crates/ironrdp/Cargo.toml @@ -31,6 +31,8 @@ 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"] +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"] 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"