Skip to content

Commit eb66fd1

Browse files
committed
feat: add QOI image codec
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 <[email protected]>
1 parent a3ab770 commit eb66fd1

File tree

19 files changed

+199
-11
lines changed

19 files changed

+199
-11
lines changed

.cargo/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[alias]
22
xtask = "run --package xtask --"
3+
4+
[patch.crates-io]
5+
qoi = { git = "https://github.com/elmarco/qoi-rust.git", branch = "raw" }

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benches/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ categories.workspace = true
1313
name = "perfenc"
1414
path = "src/perfenc.rs"
1515

16+
[features]
17+
default = ["qoi"]
18+
qoi = ["ironrdp/qoi"]
19+
1620
[dependencies]
1721
anyhow = "1.0.98"
1822
async-trait = "0.1.88"

benches/src/perfenc.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async fn main() -> Result<(), anyhow::Error> {
2727
println!(" --width <WIDTH> Width of the display (default: 3840)");
2828
println!(" --height <HEIGHT> Height of the display (default: 2400)");
2929
println!(" --codec <CODEC> Codec to use (default: remotefx)");
30-
println!(" Valid values: remotefx, bitmap, none");
30+
println!(" Valid values: qoi, remotefx, bitmap, none");
3131
println!(" --fps <FPS> Frames per second (default: none)");
3232
std::process::exit(0);
3333
}
@@ -51,6 +51,8 @@ async fn main() -> Result<(), anyhow::Error> {
5151
flags -= CmdFlags::SET_SURFACE_BITS;
5252
}
5353
OptCodec::None => {}
54+
#[cfg(feature = "qoi")]
55+
OptCodec::Qoi => update_codecs.set_qoi(Some(0)),
5456
};
5557

5658
let mut encoder = UpdateEncoder::new(DesktopSize { width, height }, flags, update_codecs);
@@ -171,6 +173,8 @@ enum OptCodec {
171173
RemoteFX,
172174
Bitmap,
173175
None,
176+
#[cfg(feature = "qoi")]
177+
Qoi,
174178
}
175179

176180
impl Default for OptCodec {
@@ -187,6 +191,8 @@ impl core::str::FromStr for OptCodec {
187191
"remotefx" => Ok(Self::RemoteFX),
188192
"bitmap" => Ok(Self::Bitmap),
189193
"none" => Ok(Self::None),
194+
#[cfg(feature = "qoi")]
195+
"qoi" => Ok(Self::Qoi),
190196
_ => Err(anyhow::anyhow!("unknown codec: {}", s)),
191197
}
192198
}

crates/ironrdp-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ test = false
2727
default = ["rustls"]
2828
rustls = ["ironrdp-tls/rustls", "tokio-tungstenite/rustls-tls-native-roots"]
2929
native-tls = ["ironrdp-tls/native-tls", "tokio-tungstenite/native-tls"]
30+
qoi = ["ironrdp/qoi"]
3031

3132
[dependencies]
3233
# Protocols

crates/ironrdp-connector/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ doctest = false
1616
test = false
1717

1818
[features]
19+
default = []
1920
arbitrary = ["dep:arbitrary"]
21+
qoi = ["ironrdp-pdu/qoi"]
2022

2123
[dependencies]
2224
ironrdp-svc = { path = "../ironrdp-svc", version = "0.3" } # public

crates/ironrdp-pdu/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ doctest = false
1919
default = []
2020
std = ["alloc", "ironrdp-error/std", "ironrdp-core/std"]
2121
alloc = ["ironrdp-core/alloc", "ironrdp-error/alloc"]
22+
qoi = []
2223

2324
[dependencies]
2425
bitflags = "2.4"

crates/ironrdp-pdu/src/rdp/capability_sets.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ pub use self::bitmap_cache::{
3434
pub use self::bitmap_codecs::{
3535
client_codecs_capabilities, server_codecs_capabilities, BitmapCodecs, CaptureFlags, Codec, CodecId, CodecProperty,
3636
EntropyBits, Guid, NsCodec, RemoteFxContainer, RfxCaps, RfxCapset, RfxClientCapsContainer, RfxICap, RfxICapFlags,
37-
CODEC_ID_NONE, CODEC_ID_REMOTEFX,
37+
CODEC_ID_NONE, CODEC_ID_QOI, CODEC_ID_REMOTEFX,
3838
};
3939
pub use self::brush::{Brush, SupportLevel};
4040
pub use self::frame_acknowledge::FrameAcknowledge;

crates/ironrdp-pdu/src/rdp/capability_sets/bitmap_codecs.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const GUID_REMOTEFX: Guid = Guid(0x7677_2f12, 0xbd72, 0x4463, 0xaf, 0xb3, 0xb7,
4040
const GUID_IMAGE_REMOTEFX: Guid = Guid(0x2744_ccd4, 0x9d8a, 0x4e74, 0x80, 0x3c, 0x0e, 0xcb, 0xee, 0xa1, 0x9c, 0x54);
4141
#[rustfmt::skip]
4242
const GUID_IGNORE: Guid = Guid(0x9c43_51a6, 0x3535, 0x42ae, 0x91, 0x0c, 0xcd, 0xfc, 0xe5, 0x76, 0x0b, 0x58);
43+
#[rustfmt::skip]
44+
#[cfg(feature="qoi")]
45+
const GUID_QOI: Guid = Guid(0x4dae_9af8, 0xb399, 0x4df6, 0xb4, 0x3a, 0x66, 0x2f, 0xd9, 0xc0, 0xf5, 0xd6);
4346

4447
#[derive(Debug, PartialEq, Eq)]
4548
pub struct Guid(u32, u16, u16, u8, u8, u8, u8, u8, u8, u8, u8);
@@ -167,6 +170,8 @@ impl Encode for Codec {
167170
CodecProperty::RemoteFx(_) => GUID_REMOTEFX,
168171
CodecProperty::ImageRemoteFx(_) => GUID_IMAGE_REMOTEFX,
169172
CodecProperty::Ignore => GUID_IGNORE,
173+
#[cfg(feature = "qoi")]
174+
CodecProperty::Qoi => GUID_QOI,
170175
_ => return Err(other_err!("invalid codec")),
171176
};
172177
guid.encode(dst)?;
@@ -204,6 +209,8 @@ impl Encode for Codec {
204209
}
205210
};
206211
}
212+
#[cfg(feature = "qoi")]
213+
CodecProperty::Qoi => dst.write_u16(0),
207214
CodecProperty::Ignore => dst.write_u16(0),
208215
CodecProperty::None => dst.write_u16(0),
209216
};
@@ -227,6 +234,8 @@ impl Encode for Codec {
227234
RemoteFxContainer::ClientContainer(container) => container.size(),
228235
RemoteFxContainer::ServerContainer(size) => *size,
229236
},
237+
#[cfg(feature = "qoi")]
238+
CodecProperty::Qoi => 0,
230239
CodecProperty::Ignore => 0,
231240
CodecProperty::None => 0,
232241
}
@@ -264,6 +273,13 @@ impl<'de> Decode<'de> for Codec {
264273
}
265274
}
266275
GUID_IGNORE => CodecProperty::Ignore,
276+
#[cfg(feature = "qoi")]
277+
GUID_QOI => {
278+
if !property_buffer.is_empty() {
279+
return Err(invalid_field_err!("qoi property", "must be empty"));
280+
}
281+
CodecProperty::Qoi
282+
}
267283
_ => CodecProperty::None,
268284
};
269285

@@ -283,6 +299,8 @@ pub enum CodecProperty {
283299
RemoteFx(RemoteFxContainer),
284300
ImageRemoteFx(RemoteFxContainer),
285301
Ignore,
302+
#[cfg(feature = "qoi")]
303+
Qoi,
286304
None,
287305
}
288306

@@ -620,12 +638,14 @@ pub struct CodecId(u8);
620638

621639
pub const CODEC_ID_NONE: CodecId = CodecId(0);
622640
pub const CODEC_ID_REMOTEFX: CodecId = CodecId(3);
641+
pub const CODEC_ID_QOI: CodecId = CodecId(0x0A);
623642

624643
impl Debug for CodecId {
625644
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
626645
let name = match self.0 {
627646
0 => "None",
628647
3 => "RemoteFx",
648+
0x0A => "QOI",
629649
_ => "unknown",
630650
};
631651
write!(f, "CodecId({})", name)
@@ -637,6 +657,7 @@ impl CodecId {
637657
match value {
638658
0 => Some(CODEC_ID_NONE),
639659
3 => Some(CODEC_ID_REMOTEFX),
660+
0x0A => Some(CODEC_ID_QOI),
640661
_ => None,
641662
}
642663
}
@@ -678,6 +699,7 @@ fn parse_codecs_config<'a>(codecs: &'a [&'a str]) -> Result<HashMap<&'a str, boo
678699
/// # List of codecs
679700
///
680701
/// * `remotefx` (on by default)
702+
/// * `qoi` (on by default, when feature "qoi")
681703
///
682704
/// # Returns
683705
///
@@ -688,6 +710,7 @@ pub fn client_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
688710
return Err(r#"
689711
List of codecs:
690712
- `remotefx` (on by default)
713+
- `qoi` (on by default, when feature "qoi")
691714
"#
692715
.to_owned());
693716
}
@@ -708,6 +731,14 @@ List of codecs:
708731
});
709732
}
710733

734+
#[cfg(feature = "qoi")]
735+
if config.remove("qoi").unwrap_or(true) {
736+
codecs.push(Codec {
737+
id: CODEC_ID_QOI.0,
738+
property: CodecProperty::Qoi,
739+
});
740+
}
741+
711742
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
712743
if !codec_names.is_empty() {
713744
return Err(format!("Unknown codecs: {}", codec_names));
@@ -729,6 +760,7 @@ List of codecs:
729760
/// # List of codecs
730761
///
731762
/// * `remotefx` (on by default)
763+
/// * `qoi` (on by default, when feature "qoi")
732764
///
733765
/// # Returns
734766
///
@@ -738,6 +770,7 @@ pub fn server_codecs_capabilities(config: &[&str]) -> Result<BitmapCodecs, Strin
738770
return Err(r#"
739771
List of codecs:
740772
- `remotefx` (on by default)
773+
- `qoi` (on by default, when feature "qoi")
741774
"#
742775
.to_owned());
743776
}
@@ -756,6 +789,14 @@ List of codecs:
756789
});
757790
}
758791

792+
#[cfg(feature = "qoi")]
793+
if config.remove("qoi").unwrap_or(true) {
794+
codecs.push(Codec {
795+
id: 0,
796+
property: CodecProperty::Qoi,
797+
});
798+
}
799+
759800
let codec_names = config.keys().copied().collect::<Vec<_>>().join(", ");
760801
if !codec_names.is_empty() {
761802
return Err(format!("Unknown codecs: {}", codec_names));

crates/ironrdp-server/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ doctest = true
1616
test = false
1717

1818
[features]
19-
default = ["rayon"]
19+
default = ["rayon", "qoi"]
2020
helper = ["dep:x509-cert", "dep:rustls-pemfile"]
2121
rayon = ["dep:rayon"]
22+
qoi = ["dep:qoi", "ironrdp-pdu/qoi"]
2223

2324
# Internal (PRIVATE!) features used to aid testing.
2425
# Don't rely on these whatsoever. They may disappear at any time.
@@ -47,6 +48,7 @@ rustls-pemfile = { version = "2.2.0", optional = true }
4748
rayon = { version = "1.10.0", optional = true }
4849
bytes = "1"
4950
visibility = { version = "0.1", optional = true }
51+
qoi = { version = "0.4", optional = true }
5052

5153
[dev-dependencies]
5254
tokio = { version = "1", features = ["sync"] }

crates/ironrdp-server/src/encoder/mod.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,30 @@ enum CodecId {
3838
#[derive(Debug)]
3939
pub(crate) struct UpdateEncoderCodecs {
4040
remotefx: Option<(EntropyBits, u8)>,
41+
#[cfg(feature = "qoi")]
42+
qoi: Option<u8>,
4143
}
4244

4345
impl UpdateEncoderCodecs {
4446
#[cfg_attr(feature = "__bench", visibility::make(pub))]
4547
pub(crate) fn new() -> Self {
46-
Self { remotefx: None }
48+
Self {
49+
remotefx: None,
50+
#[cfg(feature = "qoi")]
51+
qoi: None,
52+
}
4753
}
4854

4955
#[cfg_attr(feature = "__bench", visibility::make(pub))]
5056
pub(crate) fn set_remotefx(&mut self, remotefx: Option<(EntropyBits, u8)>) {
5157
self.remotefx = remotefx
5258
}
59+
60+
#[cfg(feature = "qoi")]
61+
#[cfg_attr(feature = "__bench", visibility::make(pub))]
62+
pub(crate) fn set_qoi(&mut self, qoi: Option<u8>) {
63+
self.qoi = qoi
64+
}
5365
}
5466

5567
impl Default for UpdateEncoderCodecs {
@@ -88,6 +100,11 @@ impl UpdateEncoder {
88100
video = Some(bitmap.clone());
89101
}
90102

103+
#[cfg(feature = "qoi")]
104+
if let Some(id) = codecs.qoi {
105+
bitmap = BitmapUpdater::Qoi(QoiHandler::new(id));
106+
}
107+
91108
(bitmap, video)
92109
} else {
93110
(BitmapUpdater::Bitmap(BitmapHandler::new()), None)
@@ -358,6 +375,8 @@ enum BitmapUpdater {
358375
None(NoneHandler),
359376
Bitmap(BitmapHandler),
360377
RemoteFx(RemoteFxHandler),
378+
#[cfg(feature = "qoi")]
379+
Qoi(QoiHandler),
361380
}
362381

363382
impl BitmapUpdater {
@@ -366,6 +385,8 @@ impl BitmapUpdater {
366385
Self::None(up) => up.handle(bitmap),
367386
Self::Bitmap(up) => up.handle(bitmap),
368387
Self::RemoteFx(up) => up.handle(bitmap),
388+
#[cfg(feature = "qoi")]
389+
Self::Qoi(up) => up.handle(bitmap),
369390
}
370391
}
371392

@@ -479,6 +500,47 @@ impl BitmapUpdateHandler for RemoteFxHandler {
479500
}
480501
}
481502

503+
#[cfg(feature = "qoi")]
504+
#[derive(Clone, Debug)]
505+
struct QoiHandler {
506+
codec_id: u8,
507+
}
508+
509+
#[cfg(feature = "qoi")]
510+
impl QoiHandler {
511+
fn new(codec_id: u8) -> Self {
512+
Self { codec_id }
513+
}
514+
}
515+
516+
#[cfg(feature = "qoi")]
517+
impl BitmapUpdateHandler for QoiHandler {
518+
fn handle(&mut self, bitmap: &BitmapUpdate) -> Result<UpdateFragmenter> {
519+
use ironrdp_graphics::image_processing::PixelFormat::*;
520+
521+
let channels = match bitmap.format {
522+
ARgb32 => qoi::RawChannels::Argb,
523+
XRgb32 => qoi::RawChannels::Xrgb,
524+
ABgr32 => qoi::RawChannels::Abgr,
525+
XBgr32 => qoi::RawChannels::Xbgr,
526+
BgrA32 => qoi::RawChannels::Bgra,
527+
BgrX32 => qoi::RawChannels::Bgrx,
528+
RgbA32 => qoi::RawChannels::Rgba,
529+
RgbX32 => qoi::RawChannels::Rgbx,
530+
};
531+
532+
let enc = qoi::Encoder::new_raw(
533+
&bitmap.data,
534+
bitmap.width.get().into(),
535+
bitmap.height.get().into(),
536+
bitmap.stride,
537+
channels,
538+
)?;
539+
let data = enc.encode_to_vec()?;
540+
set_surface(bitmap, self.codec_id, &data)
541+
}
542+
}
543+
482544
fn set_surface(bitmap: &BitmapUpdate, codec_id: u8, data: &[u8]) -> Result<UpdateFragmenter> {
483545
let destination = ExclusiveRectangle {
484546
left: bitmap.x,

0 commit comments

Comments
 (0)